diff --git a/.gitmodules b/.gitmodules index d5cd4211..18a3014b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "third_party/gopher-orch"] path = third_party/gopher-orch url = https://github.com/GopherSecurity/gopher-orch.git + branch = br_release diff --git a/.rspec_status b/.rspec_status new file mode 100644 index 00000000..e5a78b93 --- /dev/null +++ b/.rspec_status @@ -0,0 +1,119 @@ +example_id | status | run_time | +-------------------------------------------------------------------------- | ------- | --------------- | +./spec/agent_result_spec.rb[1:1:1] | passed | 0.00277 seconds | +./spec/agent_result_spec.rb[1:2:1] | passed | 0.00014 seconds | +./spec/agent_result_spec.rb[1:3:1] | passed | 0.0001 seconds | +./spec/agent_result_status_spec.rb[1:1:1] | passed | 0.00009 seconds | +./spec/agent_result_status_spec.rb[1:2:1] | passed | 0.00008 seconds | +./spec/agent_result_status_spec.rb[1:3:1] | passed | 0.00009 seconds | +./spec/agent_result_status_spec.rb[1:4:1] | passed | 0.00008 seconds | +./spec/agent_result_status_spec.rb[1:5:1] | passed | 0.00008 seconds | +./spec/agent_result_status_spec.rb[1:6:1] | passed | 0.0001 seconds | +./spec/agent_result_status_spec.rb[1:6:2] | passed | 0.00008 seconds | +./spec/config_builder_spec.rb[1:1:1] | passed | 0.00053 seconds | +./spec/config_builder_spec.rb[1:2:1:1] | passed | 0.00013 seconds | +./spec/config_builder_spec.rb[1:2:2:1] | passed | 0.0001 seconds | +./spec/config_builder_spec.rb[1:2:3:1] | passed | 0.0001 seconds | +./spec/config_builder_spec.rb[1:3:1] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:1:1] | passed | 0.00013 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:1] | passed | 0.00017 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:2] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:3] | passed | 0.00013 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:4] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:5] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:6:1] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:6:2] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:2:7:1] | passed | 0.00014 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:3:1] | passed | 0.00011 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:4:1] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:4:2] | passed | 0.00012 seconds | +./spec/gopher_orch/auth/auth_context_spec.rb[1:4:3] | passed | 0.00014 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:1] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:2] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:3] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:4] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:5] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:6] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:7] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:8] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:9] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:10] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:11] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:12] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:1:13] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:1] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:2] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:3] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:4] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:5] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:6] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:7] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:8] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:9] | passed | 0.00016 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:10] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:11] | passed | 0.00007 seconds | +./spec/gopher_orch/auth/errors_spec.rb[1:2:12] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/errors_spec.rb[2:1:1] | passed | 0.00011 seconds | +./spec/gopher_orch/auth/errors_spec.rb[2:1:2] | passed | 0.00722 seconds | +./spec/gopher_orch/auth/errors_spec.rb[2:1:3] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/errors_spec.rb[2:2:1] | passed | 0.00132 seconds | +./spec/gopher_orch/auth/errors_spec.rb[2:2:2] | passed | 0.00012 seconds | +./spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb[1:1:1] | passed | 0.00012 seconds | +./spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb[1:1:2] | passed | 0.00011 seconds | +./spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb[1:2:1] | passed | 0.00012 seconds | +./spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb[1:2:2] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb[1:2:3] | passed | 0.00102 seconds | +./spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb[1:3:1] | passed | 0.00014 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:1:1] | passed | 0.00011 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:1:2] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:1:3] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:2:1] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:2:2] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:2:3] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:3:1] | passed | 0.00011 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:3:2] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/client_registration_response_spec.rb[1:4:1] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:1:1] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:2:1] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:2:2] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:3:1] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:3:2] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:3:3] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:4:1] | passed | 0.00011 seconds | +./spec/gopher_orch/auth/oauth/openid_configuration_spec.rb[1:5:1] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:1:1] | passed | 0.00017 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:1:2] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:1:3] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:2:1] | passed | 0.00011 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:2:2] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:2:3] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:3:1] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:3:2] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb[1:4:1] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:1:1] | passed | 0.00214 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:1:2] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:1:3] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:2:1] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:2:2] | passed | 0.00012 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:2:3] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:2:4] | passed | 0.00008 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:2:5] | passed | 0.00035 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:2:6] | passed | 0.0001 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:2:7] | passed | 0.00009 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:3:1] | passed | 0.00069 seconds | +./spec/gopher_orch/auth/www_authenticate_spec.rb[1:3:2] | passed | 0.00013 seconds | +./spec/gopher_orch_spec.rb[1:1:1] | passed | 0.00082 seconds | +./spec/gopher_orch_spec.rb[1:2:1] | passed | 0.01189 seconds | +./spec/gopher_orch_spec.rb[1:3:1:1] | pending | 0.00016 seconds | +./spec/gopher_orch_spec.rb[1:4:1] | passed | 0.00012 seconds | +./spec/gopher_orch_spec.rb[1:5:1] | passed | 0.00011 seconds | +./spec/gopher_orch_spec.rb[1:5:2] | passed | 0.0009 seconds | +./spec/gopher_orch_spec.rb[1:6:1] | passed | 0.00135 seconds | +./spec/gopher_orch_spec.rb[1:7:1] | passed | 0.00014 seconds | +./spec/native_spec.rb[1:1:1] | passed | 0.0001 seconds | +./spec/native_spec.rb[1:2:1] | passed | 0.00013 seconds | +./spec/native_spec.rb[1:3:1] | passed | 6.21 seconds | +./spec/native_spec.rb[1:3:2] | passed | 1.1 seconds | +./spec/native_spec.rb[1:4:1] | passed | 2.37 seconds | +./spec/native_spec.rb[1:5:1] | passed | 0.00014 seconds | +./spec/native_spec.rb[1:6:1] | passed | 0.0001 seconds | diff --git a/.rubocop.yml b/.rubocop.yml index 22138b0d..bcb073fa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,22 +12,30 @@ AllCops: - 'examples/server*/**/*' - 'third_party/**/*' - 'native/**/*' + - 'bin/**/*' # Layout Layout/LineLength: Max: 120 + Exclude: + - 'spec/**/*' + - 'examples/**/*' + - '*.gemspec' Layout/MultilineMethodCallIndentation: EnforcedStyle: indented -# Metrics +# Metrics - relaxed for this project Metrics/BlockLength: Exclude: - 'spec/**/*' + - 'examples/**/*' - '*.gemspec' Metrics/MethodLength: - Max: 20 + Max: 25 + Exclude: + - 'examples/**/*' Metrics/ClassLength: Max: 150 @@ -35,6 +43,21 @@ Metrics/ClassLength: Metrics/AbcSize: Max: 25 +Metrics/ModuleLength: + Max: 120 + Exclude: + - 'examples/**/*' + +Metrics/CyclomaticComplexity: + Max: 10 + Exclude: + - 'examples/**/*' + +Metrics/PerceivedComplexity: + Max: 10 + Exclude: + - 'examples/**/*' + # Style Style/Documentation: Enabled: false @@ -54,18 +77,124 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: consistent_comma +Style/ClassVars: + Exclude: + - 'examples/**/*' + +Style/MultilineBlockChain: + Exclude: + - 'examples/**/*' + - 'spec/**/*' + # Naming Naming/FileName: Exclude: - 'Gemfile' - 'Rakefile' -# RSpec +Naming/AccessorMethodName: + Exclude: + - 'examples/**/*' + +Naming/PredicateMethod: + Enabled: false + +Naming/PredicatePrefix: + Enabled: false + +# Lint +Lint/ConstantDefinitionInBlock: + Exclude: + - 'spec/**/*' + +# RSpec cops RSpec/ExampleLength: - Max: 15 + Max: 25 + Exclude: + - 'examples/**/*' RSpec/MultipleExpectations: - Max: 5 + Max: 12 + Exclude: + - 'examples/**/*' RSpec/NestedGroups: Max: 4 + +RSpec/MultipleMemoizedHelpers: + Max: 10 + +RSpec/FilePath: + Enabled: false + +RSpec/SpecFilePathFormat: + Enabled: false + +RSpec/DescribeClass: + Enabled: false + +RSpec/DescribedClass: + Enabled: false + +RSpec/ContextWording: + Exclude: + - 'examples/**/*' + +RSpec/MultipleDescribes: + Exclude: + - 'examples/**/*' + - 'spec/**/*' + +RSpec/VerifiedDoubles: + Exclude: + - 'examples/**/*' + - 'spec/**/*' + +RSpec/ExpectActual: + Exclude: + - 'spec/native_spec.rb' + +RSpec/IdenticalEqualityAssertion: + Exclude: + - 'spec/native_spec.rb' + +RSpec/LeakyConstantDeclaration: + Exclude: + - 'spec/**/*' + +RSpec/PredicateMatcher: + Enabled: false + +Capybara/RSpec/PredicateMatcher: + Enabled: false + +# Disable Rails-specific cops (not using Rails) +RSpecRails/AvoidSetupHook: + Enabled: false + +RSpecRails/HaveHttpStatus: + Enabled: false + Exclude: + - 'examples/**/*' + - 'spec/**/*' + +RSpecRails/HttpStatus: + Enabled: false + +RSpecRails/InferredSpecType: + Enabled: false + +RSpecRails/MinitestAssertions: + Enabled: false + +RSpecRails/NegationBeValid: + Enabled: false + +RSpecRails/TravelAround: + Enabled: false + +# Allow OpenStruct in tests +Style/OpenStructUse: + Exclude: + - 'spec/**/*' + - 'examples/**/*' diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 00000000..246fd9aa --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,13 @@ +#!/bin/bash +# Wrapper script to run rubocop with Homebrew Ruby + +# Prefer Homebrew Ruby on macOS +if [[ "$OSTYPE" == "darwin"* ]]; then + if [ -d "/opt/homebrew/opt/ruby/bin" ]; then + export PATH="/opt/homebrew/opt/ruby/bin:$PATH" + elif [ -d "/usr/local/opt/ruby/bin" ]; then + export PATH="/usr/local/opt/ruby/bin:$PATH" + fi +fi + +exec bundle exec rubocop "$@" diff --git a/build.sh b/build.sh index f9059229..48c2ff99 100755 --- a/build.sh +++ b/build.sh @@ -344,6 +344,40 @@ else echo -e "${YELLOW}⚠ RSpec not found, skipping tests${NC}" fi +echo "" + +# Step 7: Build auth example +echo -e "${YELLOW}Step 6: Building auth example...${NC}" +AUTH_EXAMPLE_DIR="${SCRIPT_DIR}/examples/auth" + +if [ -d "${AUTH_EXAMPLE_DIR}" ] && [ -f "${AUTH_EXAMPLE_DIR}/Gemfile" ]; then + cd "${AUTH_EXAMPLE_DIR}" + + if [ "$RUBY_READY" = true ]; then + echo -e "${YELLOW} Installing auth example dependencies...${NC}" + if bundle install --quiet 2>/dev/null; then + echo -e "${GREEN}✓ Auth example dependencies installed${NC}" + + # Run auth example tests + echo -e "${YELLOW} Running auth example tests...${NC}" + if bundle exec rspec --format documentation 2>/dev/null; then + echo -e "${GREEN}✓ Auth example tests passed${NC}" + else + echo -e "${YELLOW}⚠ Some auth example tests may have failed${NC}" + fi + else + echo -e "${YELLOW}⚠ Failed to install auth example dependencies${NC}" + bundle install 2>&1 | tail -5 + fi + else + echo -e "${YELLOW}⚠ Skipping auth example (Ruby environment not ready)${NC}" + fi + + cd "${SCRIPT_DIR}" +else + echo -e "${YELLOW}⚠ Auth example not found at ${AUTH_EXAMPLE_DIR}${NC}" +fi + echo "" echo -e "${GREEN}======================================${NC}" echo -e "${GREEN}Build completed successfully!${NC}" @@ -353,3 +387,4 @@ echo -e "Native libraries: ${YELLOW}${NATIVE_LIB_DIR}${NC}" echo -e "Native headers: ${YELLOW}${NATIVE_INCLUDE_DIR}${NC}" echo -e "Run tests: ${YELLOW}bundle exec rspec${NC}" echo -e "Run example: ${YELLOW}ruby examples/client_example_json.rb${NC}" +echo -e "Run auth server: ${YELLOW}./examples/auth/run_example.sh${NC}" diff --git a/examples/auth/Gemfile b/examples/auth/Gemfile new file mode 100644 index 00000000..0b659c52 --- /dev/null +++ b/examples/auth/Gemfile @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# Use the SDK from parent directory +gem 'gopher_orch', path: '../../' + +# Web framework +gem 'json', '~> 2.6' +gem 'puma', '~> 6.0' +gem 'sinatra', '~> 3.0' + +# Rack utilities +gem 'rack', '~> 2.2' + +# Ruby 4.0 compatibility (gems removed from stdlib) +gem 'logger' +gem 'ostruct' + +group :development, :test do + gem 'rack-test', '~> 2.1' + gem 'rspec', '~> 3.12' +end diff --git a/examples/auth/Gemfile.lock b/examples/auth/Gemfile.lock new file mode 100644 index 00000000..da1d9cd8 --- /dev/null +++ b/examples/auth/Gemfile.lock @@ -0,0 +1,116 @@ +PATH + remote: ../.. + specs: + gopher_orch (0.1.0) + ffi (~> 1.15) + +GEM + remote: https://rubygems.org/ + specs: + base64 (0.3.0) + diff-lcs (1.6.2) + ffi (1.17.3) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-aarch64-linux-musl) + ffi (1.17.3-arm-linux-gnu) + ffi (1.17.3-arm-linux-musl) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86-linux-gnu) + ffi (1.17.3-x86-linux-musl) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + ffi (1.17.3-x86_64-linux-musl) + json (2.19.2) + logger (1.7.0) + mustermann (3.0.4) + ruby2_keywords (~> 0.0.1) + nio4r (2.7.5) + ostruct (0.6.3) + puma (6.6.1) + nio4r (~> 2.0) + rack (2.2.22) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) + rack-test (2.2.0) + rack (>= 1.3) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + ruby2_keywords (0.0.5) + sinatra (3.2.0) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.2.0) + tilt (~> 2.0) + tilt (2.7.0) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + gopher_orch! + json (~> 2.6) + logger + ostruct + puma (~> 6.0) + rack (~> 2.2) + rack-test (~> 2.1) + rspec (~> 3.12) + sinatra (~> 3.0) + +CHECKSUMS + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + ffi (1.17.3) sha256=0e9f39f7bb3934f77ad6feab49662be77e87eedcdeb2a3f5c0234c2938563d4c + ffi (1.17.3-aarch64-linux-gnu) sha256=28ad573df26560f0aedd8a90c3371279a0b2bd0b4e834b16a2baa10bd7a97068 + ffi (1.17.3-aarch64-linux-musl) sha256=020b33b76775b1abacc3b7d86b287cef3251f66d747092deec592c7f5df764b2 + ffi (1.17.3-arm-linux-gnu) sha256=5bd4cea83b68b5ec0037f99c57d5ce2dd5aa438f35decc5ef68a7d085c785668 + ffi (1.17.3-arm-linux-musl) sha256=0d7626bb96265f9af78afa33e267d71cfef9d9a8eb8f5525344f8da6c7d76053 + ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f + ffi (1.17.3-x86-linux-gnu) sha256=868a88fcaf5186c3a46b7c7c2b2c34550e1e61a405670ab23f5b6c9971529089 + ffi (1.17.3-x86-linux-musl) sha256=f0286aa6ef40605cf586e61406c446de34397b85dbb08cc99fdaddaef8343945 + ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5 + ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f + ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56 + gopher_orch (0.1.0) + json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mustermann (3.0.4) sha256=85fadcb6b3c6493a8b511b42426f904b7f27b282835502233dd154daab13aa22 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + puma (6.6.1) sha256=b9b56e4a4ea75d1bfa6d9e1972ee2c9f43d0883f011826d914e8e37b3694ea1e + rack (2.2.22) sha256=c5cf0b7f872559966d974abe3101a57d51caf12504ee76290b98720004f64542 + rack-protection (3.2.0) sha256=3c74ba7fc59066453d61af9bcba5b6fe7a9b3dab6f445418d3b391d5ea8efbff + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef + sinatra (3.2.0) sha256=6e727f4d034e87067d9aab37f328021d7c16722ffd293ef07b6e968915109807 + tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3 + +BUNDLED WITH + 4.0.4 diff --git a/examples/auth/README.md b/examples/auth/README.md new file mode 100644 index 00000000..1d305067 --- /dev/null +++ b/examples/auth/README.md @@ -0,0 +1,173 @@ +# Ruby Auth MCP Server + +OAuth-protected MCP (Model Context Protocol) server example using the GopherOrch SDK. + +## Overview + +This example demonstrates: +- OAuth 2.0 protected resource metadata (RFC 9728) +- Authorization server metadata (RFC 8414) +- OpenID Connect discovery +- Dynamic client registration (RFC 7591) +- Scope-based access control for MCP tools +- JSON-RPC 2.0 MCP protocol implementation + +## Setup + +### Prerequisites + +- Ruby 3.0 or later +- Bundler gem + +### Install Dependencies + +```bash +cd examples/auth +bundle install +``` + +## Configuration + +Copy the example configuration file: + +```bash +cp server.config.example server.config +``` + +Edit `server.config` to configure: + +- `host` - Server bind address (default: 0.0.0.0) +- `port` - Server port (default: 8080) +- `server_url` - Public URL of this server +- `auth_server_url` - OAuth authorization server URL +- `auth_disabled` - Set to `true` to disable authentication +- `allowed_scopes` - Space-separated list of supported OAuth scopes + +## Running the Server + +### Using the run script (recommended) + +```bash +./run_example.sh [config_path] +``` + +Or on Windows: + +```powershell +.\run_example.ps1 [config_path] +``` + +### Direct execution + +```bash +bundle exec ruby lib/auth_mcp_server.rb [config_path] +``` + +## API Endpoints + +### Discovery Endpoints (No Auth Required) + +```bash +# Health check +curl http://localhost:8080/health + +# Protected resource metadata +curl http://localhost:8080/.well-known/oauth-protected-resource + +# Authorization server metadata +curl http://localhost:8080/.well-known/oauth-authorization-server + +# OpenID Connect configuration +curl http://localhost:8080/.well-known/openid-configuration +``` + +### OAuth Endpoints + +```bash +# Dynamic client registration +curl -X POST http://localhost:8080/oauth/register \ + -H "Content-Type: application/json" \ + -d '{"redirect_uris": ["http://localhost:3000/callback"]}' +``` + +### MCP Endpoints (Auth Required When Enabled) + +```bash +# Initialize MCP session +curl -X POST http://localhost:8080/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}' + +# List available tools +curl -X POST http://localhost:8080/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}' + +# Call a tool +curl -X POST http://localhost:8080/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "get-weather", "arguments": {"city": "London"}}}' +``` + +## Available Tools + +### get-weather (No scope required) +Get current weather for a city. + +```json +{"name": "get-weather", "arguments": {"city": "London"}} +``` + +### get-forecast (Requires mcp:read scope) +Get 5-day weather forecast for a city. + +```json +{"name": "get-forecast", "arguments": {"city": "London", "days": 5}} +``` + +### get-weather-alerts (Requires mcp:admin scope) +Get weather alerts for a region. + +```json +{"name": "get-weather-alerts", "arguments": {"region": "California"}} +``` + +## Testing + +Run the test suite: + +```bash +bundle exec rspec +``` + +Run specific tests: + +```bash +bundle exec rspec spec/routes/health_spec.rb +bundle exec rspec spec/tools/weather_tools_spec.rb +``` + +## Architecture + +``` +lib/ +├── auth_mcp_server.rb # Main application entry point +├── config.rb # Configuration loading +├── middleware/ +│ ├── cors_middleware.rb # CORS handling +│ └── oauth_auth_middleware.rb # OAuth authentication +├── routes/ +│ ├── health.rb # Health endpoint +│ ├── oauth_endpoints.rb # OAuth discovery endpoints +│ ├── mcp_handler.rb # JSON-RPC handler +│ └── mcp_endpoints.rb # MCP HTTP endpoints +└── tools/ + └── weather_tools.rb # Example MCP tools +``` + +## License + +See the main repository license. diff --git a/examples/auth/lib/auth_mcp_server.rb b/examples/auth/lib/auth_mcp_server.rb new file mode 100644 index 00000000..f82e457e --- /dev/null +++ b/examples/auth/lib/auth_mcp_server.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +# Auth MCP Server - OAuth-protected MCP server example +# Demonstrates usage of the GopherOrch SDK auth components. + +require 'sinatra/base' +require 'json' + +# Require SDK components +require 'gopher_orch/auth/auth_context' +require 'gopher_orch/auth/errors' +require 'gopher_orch/auth/www_authenticate' +require 'gopher_orch/auth/oauth/protected_resource_metadata' +require 'gopher_orch/auth/oauth/authorization_server_metadata' +require 'gopher_orch/auth/oauth/openid_configuration' +require 'gopher_orch/auth/oauth/client_registration_response' + +# Require local components +require_relative 'config' +require_relative 'middleware/cors_middleware' +require_relative 'middleware/oauth_auth_middleware' +require_relative 'routes/health' +require_relative 'routes/oauth_endpoints' +require_relative 'routes/mcp_handler' +require_relative 'routes/mcp_endpoints' +require_relative 'tools/weather_tools' + +module AuthMcpServer + VERSION = '1.0.0' + + # Main application class for the Auth MCP Server. + # Extends Sinatra::Base and configures middleware and routes. + class App < Sinatra::Base + attr_reader :config, :mcp_handler + + # Configures the application with the given config. + # + # @param config [Config] the server configuration + def self.setup(config) + @@config = config + + # Create MCP handler + server_info = { + name: 'ruby-auth-mcp-server', + version: VERSION, + } + @@mcp_handler = McpHandler.new(server_info) + + # Register weather tools + WeatherTools.register(@@mcp_handler, config) + + # Store in settings + set :config, config + set :mcp_handler, @@mcp_handler + + # Configure middleware + use CorsMiddleware + use OAuthAuthMiddleware, config + + # Register routes + register Routes::Health + register Routes::OAuthEndpoints + register Routes::McpEndpoints + end + + # Returns the server configuration. + # + # @return [Config] the configuration + def self.server_config + @@config + end + + # Returns the MCP handler. + # + # @return [McpHandler] the MCP handler + def self.mcp_handler_instance + @@mcp_handler + end + end + + # Prints the startup banner. + def self.print_banner + puts '' + puts '========================================' + puts ' Ruby Auth MCP Server' + puts ' OAuth-Protected MCP Example' + puts '========================================' + puts '' + end + + # Prints available endpoints. + # + # @param config [Config] the server configuration + def self.print_endpoints(config) + base_url = config.server_url + + puts 'Endpoints:' + puts " Health: GET #{base_url}/health" + puts " OAuth Meta: GET #{base_url}/.well-known/oauth-protected-resource" + puts " Auth Server: GET #{base_url}/.well-known/oauth-authorization-server" + puts " OIDC Config: GET #{base_url}/.well-known/openid-configuration" + puts " OAuth Auth: GET #{base_url}/oauth/authorize" + puts " Register: POST #{base_url}/oauth/register" + puts " MCP: POST #{base_url}/mcp" + puts " RPC: POST #{base_url}/rpc" + puts '' + end + + # Prints authentication status. + # + # @param config [Config] the server configuration + def self.print_auth_status(config) + if config.auth_disabled + puts 'Authentication: DISABLED' + else + puts 'Authentication: ENABLED' + puts " JWKS URI: #{config.jwks_uri}" if config.jwks_uri + puts " Issuer: #{config.issuer}" if config.issuer + end + puts '' + end + + # Starts the server. + # + # @param config_path [String] path to configuration file + def self.start(config_path = nil) + print_banner + + # Determine config path + config_path ||= File.expand_path('../server.config', __dir__) + + # Load configuration + puts "Loading configuration from: #{config_path}" + begin + config = Config.load_from_file(config_path) + puts 'Configuration loaded successfully' + puts '' + rescue StandardError => e + puts "Failed to load configuration: #{e.message}" + exit 1 + end + + # Setup application + App.setup(config) + + # Print info + print_endpoints(config) + print_auth_status(config) + + puts "Starting server on #{config.host}:#{config.port}..." + puts 'Press Ctrl+C to shutdown' + puts '' + + # Start Sinatra + App.run!(host: config.host, port: config.port, server: 'puma') + end +end + +# Entry point when run directly +if __FILE__ == $PROGRAM_NAME + config_path = ARGV[0] + AuthMcpServer.start(config_path) +end diff --git a/examples/auth/lib/config.rb b/examples/auth/lib/config.rb new file mode 100644 index 00000000..a33d7238 --- /dev/null +++ b/examples/auth/lib/config.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +module AuthMcpServer + # Configuration for the Auth MCP Server. + # Loads settings from an INI-style configuration file. + class Config + attr_accessor :host, :port, :server_url, :auth_server_url, + :jwks_uri, :issuer, :client_id, :client_secret, + :oauth_authorize_url, :oauth_token_url, + :allowed_scopes, :exchange_idps, + :jwks_cache_duration, :jwks_auto_refresh, + :request_timeout, :auth_disabled + + # Default configuration values + DEFAULTS = { + host: '0.0.0.0', + port: 3000, + server_url: 'http://localhost:3000', + auth_server_url: '', + jwks_uri: '', + issuer: '', + client_id: '', + client_secret: '', + oauth_authorize_url: '', + oauth_token_url: '', + allowed_scopes: 'openid profile email', + exchange_idps: '', + jwks_cache_duration: 3600, + jwks_auto_refresh: true, + request_timeout: 30, + auth_disabled: true, + }.freeze + + def initialize(**attrs) + DEFAULTS.each do |key, default_value| + instance_variable_set("@#{key}", attrs.fetch(key, default_value)) + end + derive_endpoints + end + + # Loads configuration from a file. + # + # @param path [String] path to the configuration file + # @return [Config] the loaded configuration + def self.load_from_file(path) + return new unless File.exist?(path) + + content = File.read(path) + attrs = parse_config_file(content) + new(**attrs) + end + + # Parses an INI-style configuration file. + # + # @param content [String] the file content + # @return [Hash] parsed configuration as symbol keys + def self.parse_config_file(content) + attrs = {} + + content.each_line do |line| + line = line.strip + + # Skip empty lines and comments + next if line.empty? || line.start_with?('#') + + # Parse key=value pairs + next unless line.include?('=') + + key, value = line.split('=', 2) + key = key.strip.to_sym + value = value.strip + + # Convert types + attrs[key] = convert_value(key, value) + end + + attrs + end + + # Converts string values to appropriate types. + # + # @param key [Symbol] the configuration key + # @param value [String] the string value + # @return [Object] the converted value + def self.convert_value(key, value) + case key + when :port, :jwks_cache_duration, :request_timeout + value.to_i + when :auth_disabled, :jwks_auto_refresh + %w[true 1 yes].include?(value.downcase) + else + value + end + end + + # Derives server_url and OAuth endpoints if not explicitly set. + def derive_endpoints + # Derive server_url from host and port if not explicitly set + if @server_url == DEFAULTS[:server_url] || @server_url.nil? || @server_url.empty? + host_for_url = @host == '0.0.0.0' ? 'localhost' : @host + @server_url = "http://#{host_for_url}:#{@port}" + end + + return if @auth_server_url.nil? || @auth_server_url.empty? + + base_url = @auth_server_url.chomp('/') + + @jwks_uri = "#{base_url}/protocol/openid-connect/certs" if @jwks_uri.nil? || @jwks_uri.empty? + + @issuer = base_url if @issuer.nil? || @issuer.empty? + + if @oauth_authorize_url.nil? || @oauth_authorize_url.empty? + @oauth_authorize_url = "#{base_url}/protocol/openid-connect/auth" + end + + return unless @oauth_token_url.nil? || @oauth_token_url.empty? + + @oauth_token_url = "#{base_url}/protocol/openid-connect/token" + end + + # Returns true if authentication is enabled. + # + # @return [Boolean] + def auth_enabled? + !@auth_disabled + end + end +end diff --git a/examples/auth/lib/middleware/cors_middleware.rb b/examples/auth/lib/middleware/cors_middleware.rb new file mode 100644 index 00000000..b65dafb6 --- /dev/null +++ b/examples/auth/lib/middleware/cors_middleware.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module AuthMcpServer + # CORS utilities and constants for cross-origin request handling. + module Cors + # Standard CORS headers applied to all responses. + # Includes MCP-specific headers for protocol support. + STANDARD_HEADERS = { + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD', + 'Access-Control-Allow-Headers' => %w[ + Accept + Accept-Language + Content-Language + Content-Type + Authorization + X-Requested-With + Origin + Cache-Control + Pragma + Mcp-Session-Id + Mcp-Protocol-Version + ].join(', '), + 'Access-Control-Expose-Headers' => 'WWW-Authenticate, Content-Length, Content-Type', + 'Access-Control-Max-Age' => '86400', + }.freeze + + # Returns a CORS preflight response. + # Used for OPTIONS requests. + # + # @return [Array] Rack response tuple [status, headers, body] + def self.preflight_response + [204, STANDARD_HEADERS.dup, ['']] + end + end + + # Rack middleware for handling CORS (Cross-Origin Resource Sharing). + # Automatically handles OPTIONS preflight requests and adds CORS headers + # to all responses. + class CorsMiddleware + # Creates a new CorsMiddleware instance. + # + # @param app [#call] the Rack application to wrap + def initialize(app) + @app = app + end + + # Handles incoming requests. + # Returns preflight response for OPTIONS requests, + # otherwise adds CORS headers to the response. + # + # @param env [Hash] the Rack environment + # @return [Array] Rack response tuple [status, headers, body] + def call(env) + # Handle preflight requests + return Cors.preflight_response if env['REQUEST_METHOD'] == 'OPTIONS' + + # Call the application and add CORS headers to response + status, headers, body = @app.call(env) + + # Merge CORS headers into response + Cors::STANDARD_HEADERS.each do |key, value| + headers[key] = value + end + + [status, headers, body] + end + end +end diff --git a/examples/auth/lib/middleware/oauth_auth_middleware.rb b/examples/auth/lib/middleware/oauth_auth_middleware.rb new file mode 100644 index 00000000..d71b828d --- /dev/null +++ b/examples/auth/lib/middleware/oauth_auth_middleware.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require_relative 'cors_middleware' + +# SDK auth components +require 'gopher_orch/auth/auth_context' +require 'gopher_orch/auth/www_authenticate' + +module AuthMcpServer + # Rack middleware for OAuth authentication. + # Validates bearer tokens and populates auth context for downstream handlers. + class OAuthAuthMiddleware + # Paths that never require authentication + PUBLIC_PATHS = %w[/health /favicon.ico].freeze + + # Path prefixes that don't require authentication + PUBLIC_PREFIXES = %w[/.well-known/ /oauth/].freeze + + # Path prefixes that require authentication + PROTECTED_PREFIXES = %w[/mcp /rpc /events /sse].freeze + + # Creates a new OAuthAuthMiddleware instance. + # + # @param app [#call] the Rack application to wrap + # @param config [Config] the server configuration + def initialize(app, config) + @app = app + @config = config + end + + # Handles incoming requests. + # Checks authentication for protected paths and populates auth context. + # + # @param env [Hash] the Rack environment + # @return [Array] Rack response tuple [status, headers, body] + def call(env) + path = env['PATH_INFO'] + + # Handle CORS preflight (handled by CorsMiddleware, but check here too) + return Cors.preflight_response if env['REQUEST_METHOD'] == 'OPTIONS' + + # Check if path requires authentication + unless requires_auth?(path) + env['auth_context'] = GopherOrch::Auth::AuthContext.empty + return @app.call(env) + end + + # Auth disabled - allow with anonymous context + if @config.auth_disabled + env['auth_context'] = GopherOrch::Auth::AuthContext.anonymous(@config.allowed_scopes) + return @app.call(env) + end + + # Extract token + token = extract_token(env) + return send_unauthorized('invalid_request', 'Missing bearer token') unless token + + # TODO: Validate token with GopherAuthClient (to be implemented) + # For now, create anonymous context when auth is not disabled + # Real implementation would validate JWT and extract claims + + env['auth_context'] = GopherOrch::Auth::AuthContext.anonymous(@config.allowed_scopes) + @app.call(env) + end + + private + + # Checks if a path requires authentication. + # + # @param path [String] the request path + # @return [Boolean] true if authentication is required + def requires_auth?(path) + # Public paths never require auth + return false if PUBLIC_PATHS.include?(path) + + # Public prefixes don't require auth + return false if PUBLIC_PREFIXES.any? { |prefix| path.start_with?(prefix) } + + # Check if path matches protected prefixes + PROTECTED_PREFIXES.any? do |prefix| + path == prefix || path.start_with?("#{prefix}/") + end + end + + # Extracts bearer token from request. + # Checks Authorization header first, then access_token query parameter. + # + # @param env [Hash] the Rack environment + # @return [String, nil] the bearer token or nil if not found + def extract_token(env) + # Check Authorization header + auth_header = env['HTTP_AUTHORIZATION'] + return auth_header[7..] if auth_header&.start_with?('Bearer ') + + # Check query parameter + query_string = env['QUERY_STRING'] || '' + params = parse_query_string(query_string) + params['access_token'] + end + + # Parses query string into hash. + # + # @param query_string [String] the query string + # @return [Hash] parsed parameters + def parse_query_string(query_string) + params = {} + query_string.split('&').each do |pair| + key, value = pair.split('=', 2) + next unless key + + params[URI.decode_www_form_component(key)] = value ? URI.decode_www_form_component(value) : '' + end + params + end + + # Sends an unauthorized response with WWW-Authenticate header. + # Uses SDK's WwwAuthenticate helper to generate RFC 6750 Bearer challenge. + # + # @param error [String] the error code (e.g., 'invalid_request', 'invalid_token') + # @param description [String] human-readable error description + # @return [Array] Rack response tuple [401, headers, [body]] + def send_unauthorized(error, description) + # Build WWW-Authenticate header using SDK helper + www_authenticate = GopherOrch::Auth::WwwAuthenticate.generate( + realm: @config.server_url, + resource_metadata_url: "#{@config.server_url}/.well-known/oauth-protected-resource", + scopes: @config.allowed_scopes, + error: error, + description: description + ) + + headers = { + 'WWW-Authenticate' => www_authenticate, + 'Content-Type' => 'application/json', + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD', + 'Access-Control-Allow-Headers' => Cors::STANDARD_HEADERS['Access-Control-Allow-Headers'], + 'Access-Control-Expose-Headers' => 'WWW-Authenticate, Content-Length, Content-Type', + } + + body = { error: error, error_description: description }.to_json + [401, headers, [body]] + end + end +end diff --git a/examples/auth/lib/routes/health.rb b/examples/auth/lib/routes/health.rb new file mode 100644 index 00000000..9b450225 --- /dev/null +++ b/examples/auth/lib/routes/health.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'json' +require 'time' +require_relative '../middleware/cors_middleware' + +module AuthMcpServer + module Routes + # Health check endpoint for monitoring and load balancer probes. + module Health + # Registers health routes with the Sinatra application. + # + # @param app [Sinatra::Base] the Sinatra application + def self.registered(app) + # GET /health - Health check endpoint + app.get '/health' do + content_type :json + + { + status: 'ok', + timestamp: Time.now.utc.iso8601, + version: AuthMcpServer::VERSION, + }.to_json + end + + # OPTIONS /health - CORS preflight + app.options '/health' do + status, headers, body = Cors.preflight_response + headers.each { |k, v| response.headers[k] = v } + status status + body.first + end + end + end + end +end diff --git a/examples/auth/lib/routes/mcp_endpoints.rb b/examples/auth/lib/routes/mcp_endpoints.rb new file mode 100644 index 00000000..17d6f91a --- /dev/null +++ b/examples/auth/lib/routes/mcp_endpoints.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'sinatra/base' +require 'json' +require_relative 'mcp_handler' +require_relative '../middleware/cors_middleware' + +module AuthMcpServer + module Routes + # Sinatra extension for MCP HTTP endpoints. + # Provides JSON-RPC 2.0 endpoints for MCP protocol communication. + module McpEndpoints + # Registers the MCP endpoints with the Sinatra application. + # + # @param app [Sinatra::Base] the Sinatra application + def self.registered(app) + # Include helper methods in the app + app.helpers Helpers + + # OPTIONS /mcp - CORS preflight + app.options '/mcp' do + cors_preflight_response + end + + # OPTIONS /rpc - CORS preflight + app.options '/rpc' do + cors_preflight_response + end + + # POST /mcp - Main MCP endpoint + app.post '/mcp' do + handle_mcp_request + end + + # POST /rpc - Alias for /mcp + app.post '/rpc' do + handle_mcp_request + end + end + + # Helper methods mixed into the Sinatra app + module Helpers + # Returns CORS preflight response. + def cors_preflight_response + s, hdrs, body = AuthMcpServer::Cors.preflight_response + hdrs.each { |k, v| response.headers[k] = v } + status s + body.first + end + + # Handles an MCP JSON-RPC request. + # Reads the request body, passes to MCP handler, and returns JSON response. + # + # @return [String] JSON-RPC response + def handle_mcp_request + content_type :json + + # Add CORS headers + AuthMcpServer::Cors::STANDARD_HEADERS.each do |key, value| + headers[key] = value + end + + # Read request body + body = request.body.read + + # Get auth context from middleware + auth_context = env['auth_context'] + + # Get MCP handler from app settings + mcp_handler = settings.mcp_handler + + # Handle request + result = mcp_handler.handle_request(body, auth_context) + result.to_json + end + end + end + end +end diff --git a/examples/auth/lib/routes/mcp_handler.rb b/examples/auth/lib/routes/mcp_handler.rb new file mode 100644 index 00000000..e6d48936 --- /dev/null +++ b/examples/auth/lib/routes/mcp_handler.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +require 'json' + +module AuthMcpServer + # JSON-RPC 2.0 error codes + module JsonRpcErrorCodes + PARSE_ERROR = -32_700 + INVALID_REQUEST = -32_600 + METHOD_NOT_FOUND = -32_601 + INVALID_PARAMS = -32_602 + INTERNAL_ERROR = -32_603 + end + + # JSON-RPC 2.0 error exception + class JsonRpcError < StandardError + attr_reader :code, :data + + # Creates a new JsonRpcError. + # + # @param code [Integer] the error code + # @param message [String] the error message + # @param data [Object, nil] optional additional error data + def initialize(code, message, data = nil) + @code = code + @data = data + super(message) + end + + # Converts to hash for JSON response. + # + # @return [Hash] the error as a hash + def to_h + result = { code: @code, message: message } + result[:data] = @data if @data + result + end + end + + # MCP (Model Context Protocol) handler for JSON-RPC 2.0 requests. + class McpHandler + attr_reader :server_info + + # Creates a new McpHandler. + # + # @param server_info [Hash] server information for initialize response + def initialize(server_info) + @server_info = server_info + @tools = {} + end + + # Parses and validates a JSON-RPC 2.0 request. + # + # @param body [String] the raw request body + # @return [Hash] parsed request with :id, :method, :params + # @raise [JsonRpcError] if parsing or validation fails + def parse_request(body) + # Parse JSON + request = begin + JSON.parse(body) + rescue JSON::ParserError => e + raise JsonRpcError.new( + JsonRpcErrorCodes::PARSE_ERROR, + 'Parse error: Invalid JSON', + e.message + ) + end + + # Validate jsonrpc version + unless request['jsonrpc'] == '2.0' + raise JsonRpcError.new( + JsonRpcErrorCodes::INVALID_REQUEST, + 'Invalid Request: Missing or invalid jsonrpc version' + ) + end + + # Validate method + method = request['method'] + unless method.is_a?(String) && !method.empty? + raise JsonRpcError.new( + JsonRpcErrorCodes::INVALID_REQUEST, + 'Invalid Request: Missing or invalid method' + ) + end + + { + id: request['id'], + method: method, + params: request['params'] || {}, + } + end + + # Builds a JSON-RPC 2.0 success response. + # + # @param id [Object] the request ID + # @param result [Object] the result data + # @return [Hash] the response + def build_response(id, result) + { + jsonrpc: '2.0', + id: id, + result: result, + } + end + + # Builds a JSON-RPC 2.0 error response. + # + # @param id [Object] the request ID + # @param error [JsonRpcError, Hash] the error + # @return [Hash] the response + def build_error_response(id, error) + error_hash = error.is_a?(JsonRpcError) ? error.to_h : error + + { + jsonrpc: '2.0', + id: id, + error: error_hash, + } + end + + # Registers a tool with the handler. + # + # @param name [String] the tool name + # @param spec [Hash] the tool specification (description, inputSchema) + # @yield [arguments, auth_context] block to execute when tool is called + def register_tool(name, spec, &handler) + @tools[name] = { + spec: spec, + handler: handler, + } + end + + # Returns all registered tools with their specifications. + # + # @return [Array] array of tool specs with name field + def get_tools + @tools.map do |name, tool| + { name: name }.merge(tool[:spec]) + end + end + + # Handles a JSON-RPC request and returns a response. + # + # @param body [String] the raw request body + # @param auth_context [AuthContext] the authentication context + # @return [Hash] the JSON-RPC response + def handle_request(body, auth_context) + request = parse_request(body) + result = dispatch_method(request[:method], request[:params], auth_context) + build_response(request[:id], result) + rescue JsonRpcError => e + build_error_response(request&.dig(:id), e) + rescue StandardError => e + error = JsonRpcError.new( + JsonRpcErrorCodes::INTERNAL_ERROR, + 'Internal error', + e.message + ) + build_error_response(request&.dig(:id), error) + end + + # Dispatches a method call to the appropriate handler. + # + # @param method [String] the method name + # @param params [Hash] the method parameters + # @param auth_context [AuthContext] the authentication context + # @return [Object] the method result + # @raise [JsonRpcError] if method not found + def dispatch_method(method, params, auth_context) + case method + when 'initialize' + handle_initialize(params) + when 'tools/list' + handle_tools_list(params) + when 'tools/call' + handle_tools_call(params, auth_context) + when 'ping' + handle_ping + else + raise JsonRpcError.new( + JsonRpcErrorCodes::METHOD_NOT_FOUND, + "Method not found: #{method}" + ) + end + end + + private + + # Handles the initialize method. + # Returns protocol version, capabilities, and server info. + # + # @param params [Hash] the method parameters (unused) + # @return [Hash] initialize response with protocolVersion, capabilities, serverInfo + def handle_initialize(_params) + { + protocolVersion: '2024-11-05', + capabilities: { tools: {} }, + serverInfo: @server_info, + } + end + + # Handles the tools/list method. + # Returns all registered tools. + # + # @param params [Hash] the method parameters (unused) + # @return [Hash] response with tools array + def handle_tools_list(_params) + { tools: get_tools } + end + + # Handles the tools/call method. + # Executes the specified tool with the given arguments. + # + # @param params [Hash] the method parameters (name, arguments) + # @param auth_context [AuthContext] the authentication context + # @return [Object] the tool execution result + # @raise [JsonRpcError] if tool name is missing or tool not found + def handle_tools_call(params, auth_context) + name = params['name'] + unless name + raise JsonRpcError.new( + JsonRpcErrorCodes::INVALID_PARAMS, + 'Missing required parameter: name' + ) + end + + tool = @tools[name] + unless tool + raise JsonRpcError.new( + JsonRpcErrorCodes::METHOD_NOT_FOUND, + "Tool not found: #{name}" + ) + end + + arguments = params['arguments'] || {} + tool[:handler].call(arguments, auth_context) + end + + # Handles the ping method. + # + # @return [Hash] empty hash + def handle_ping + {} + end + end +end diff --git a/examples/auth/lib/routes/oauth_endpoints.rb b/examples/auth/lib/routes/oauth_endpoints.rb new file mode 100644 index 00000000..eb2af730 --- /dev/null +++ b/examples/auth/lib/routes/oauth_endpoints.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../middleware/cors_middleware' + +# SDK OAuth models +require 'gopher_orch/auth/oauth/protected_resource_metadata' +require 'gopher_orch/auth/oauth/authorization_server_metadata' +require 'gopher_orch/auth/oauth/openid_configuration' +require 'gopher_orch/auth/oauth/client_registration_response' + +module AuthMcpServer + module Routes + # OAuth discovery and registration endpoints. + # Implements RFC 9728, RFC 8414, and RFC 7591. + module OAuthEndpoints + # Registers OAuth routes with the Sinatra application. + # + # @param app [Sinatra::Base] the Sinatra application + def self.registered(app) + # Include helper methods in the app + app.helpers Helpers + + register_protected_resource_metadata(app) + register_authorization_server_metadata(app) + register_openid_configuration(app) + register_authorize_endpoint(app) + register_client_registration(app) + end + + # Registers the authorization redirect endpoint. + def self.register_authorize_endpoint(app) + # GET /oauth/authorize - Redirect to OAuth provider + app.get '/oauth/authorize' do + config = settings.config + authorize_url = derive_authorization_endpoint(config) + + # Forward all query parameters to the authorization server + query_string = request.query_string + redirect_url = query_string.empty? ? authorize_url : "#{authorize_url}?#{query_string}" + + redirect redirect_url, 302 + end + + # OPTIONS handler for CORS + app.options '/oauth/authorize' do + cors_preflight_response + end + end + + # Registers the dynamic client registration endpoint (RFC 7591). + def self.register_client_registration(app) + # POST /oauth/register - Dynamic client registration + app.post '/oauth/register' do + content_type :json + config = settings.config + + # Parse request body for redirect_uris + body = begin + JSON.parse(request.body.read) + rescue JSON::ParserError + {} + end + + registration = build_client_registration_response(config, body) + + status 201 + registration.to_json + end + + # OPTIONS handler for CORS + app.options '/oauth/register' do + cors_preflight_response + end + end + + # Registers Authorization Server Metadata endpoints (RFC 8414). + def self.register_authorization_server_metadata(app) + # GET /.well-known/oauth-authorization-server + app.get '/.well-known/oauth-authorization-server' do + content_type :json + build_authorization_server_metadata(settings.config).to_json + end + + # OPTIONS handler for CORS + app.options '/.well-known/oauth-authorization-server' do + cors_preflight_response + end + end + + # Registers OpenID Connect discovery endpoint. + def self.register_openid_configuration(app) + # GET /.well-known/openid-configuration + app.get '/.well-known/openid-configuration' do + content_type :json + build_openid_configuration(settings.config).to_json + end + + # OPTIONS handler for CORS + app.options '/.well-known/openid-configuration' do + cors_preflight_response + end + end + + # Registers Protected Resource Metadata endpoints (RFC 9728). + def self.register_protected_resource_metadata(app) + # GET /.well-known/oauth-protected-resource + app.get '/.well-known/oauth-protected-resource' do + content_type :json + build_protected_resource_metadata(settings.config).to_json + end + + # GET /.well-known/oauth-protected-resource/mcp + app.get '/.well-known/oauth-protected-resource/mcp' do + content_type :json + build_protected_resource_metadata(settings.config).to_json + end + + # OPTIONS handlers for CORS + app.options '/.well-known/oauth-protected-resource' do + cors_preflight_response + end + + app.options '/.well-known/oauth-protected-resource/mcp' do + cors_preflight_response + end + end + + # Helper methods mixed into the Sinatra app + module Helpers + # Builds Protected Resource Metadata from config. + # + # @param config [Config] the server configuration + # @return [GopherOrch::Auth::OAuth::ProtectedResourceMetadata] + def build_protected_resource_metadata(config) + GopherOrch::Auth::OAuth::ProtectedResourceMetadata.new( + resource: "#{config.server_url}/mcp", + authorization_servers: [config.server_url], + scopes_supported: config.allowed_scopes.split, + bearer_methods_supported: %w[header query], + resource_documentation: "#{config.server_url}/docs" + ) + end + + # Returns CORS preflight response. + def cors_preflight_response + s, headers, body = Cors.preflight_response + headers.each { |k, v| response.headers[k] = v } + status s + body.first + end + + # Builds Authorization Server Metadata from config. + # + # @param config [Config] the server configuration + # @return [GopherOrch::Auth::OAuth::AuthorizationServerMetadata] + def build_authorization_server_metadata(config) + issuer = config.issuer.to_s.empty? ? config.server_url : config.issuer + + GopherOrch::Auth::OAuth::AuthorizationServerMetadata.new( + issuer: issuer, + authorization_endpoint: derive_authorization_endpoint(config), + token_endpoint: derive_token_endpoint(config), + jwks_uri: config.jwks_uri, + registration_endpoint: "#{config.server_url}/oauth/register", + scopes_supported: config.allowed_scopes.split, + response_types_supported: ['code'], + grant_types_supported: %w[authorization_code refresh_token], + token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post none], + code_challenge_methods_supported: ['S256'] + ) + end + + # Builds OpenID Configuration from config. + # Merges OIDC scopes with config scopes. + # + # @param config [Config] the server configuration + # @return [GopherOrch::Auth::OAuth::OpenIdConfiguration] + def build_openid_configuration(config) + issuer = config.issuer.to_s.empty? ? config.server_url : config.issuer + + # Merge OIDC scopes with config scopes + oidc_scopes = %w[openid profile email] + config_scopes = config.allowed_scopes.split + all_scopes = (oidc_scopes + config_scopes).uniq + + GopherOrch::Auth::OAuth::OpenIdConfiguration.new( + issuer: issuer, + authorization_endpoint: derive_authorization_endpoint(config), + token_endpoint: derive_token_endpoint(config), + jwks_uri: config.jwks_uri, + registration_endpoint: "#{config.server_url}/oauth/register", + scopes_supported: all_scopes, + response_types_supported: ['code'], + grant_types_supported: %w[authorization_code refresh_token], + token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post none], + code_challenge_methods_supported: ['S256'], + userinfo_endpoint: derive_userinfo_endpoint(config), + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: %w[RS256 ES256] + ) + end + + # Derives authorization endpoint from config. + def derive_authorization_endpoint(config) + return config.oauth_authorize_url unless config.oauth_authorize_url.to_s.empty? + return "#{config.auth_server_url}/protocol/openid-connect/auth" unless config.auth_server_url.to_s.empty? + + "#{config.server_url}/oauth/authorize" + end + + # Derives token endpoint from config. + def derive_token_endpoint(config) + return config.oauth_token_url unless config.oauth_token_url.to_s.empty? + return "#{config.auth_server_url}/protocol/openid-connect/token" unless config.auth_server_url.to_s.empty? + + "#{config.server_url}/oauth/token" + end + + # Derives userinfo endpoint from config. + def derive_userinfo_endpoint(config) + return "#{config.auth_server_url}/protocol/openid-connect/userinfo" unless config.auth_server_url.to_s.empty? + + nil + end + + # Builds Client Registration Response from config. + # + # @param config [Config] the server configuration + # @param body [Hash] the registration request body + # @return [GopherOrch::Auth::OAuth::ClientRegistrationResponse] + def build_client_registration_response(config, body) + client_secret = config.client_secret.to_s.empty? ? nil : config.client_secret + auth_method = client_secret ? 'client_secret_post' : 'none' + + GopherOrch::Auth::OAuth::ClientRegistrationResponse.new( + client_id: config.client_id, + client_secret: client_secret, + client_id_issued_at: Time.now.to_i, + client_secret_expires_at: 0, + redirect_uris: body['redirect_uris'] || [], + grant_types: %w[authorization_code refresh_token], + response_types: ['code'], + token_endpoint_auth_method: auth_method + ) + end + end + end + end +end diff --git a/examples/auth/lib/tools/weather_tools.rb b/examples/auth/lib/tools/weather_tools.rb new file mode 100644 index 00000000..821e0e05 --- /dev/null +++ b/examples/auth/lib/tools/weather_tools.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require 'json' + +module AuthMcpServer + # Weather tools demonstrating OAuth scope-based access control. + # Provides simulated weather data for testing MCP tool functionality. + module WeatherTools + # Weather conditions for simulation + CONDITIONS = ['Sunny', 'Cloudy', 'Rainy', 'Partly Cloudy', 'Windy', 'Stormy'].freeze + + # Registers all weather tools with the MCP handler. + # + # @param mcp_handler [McpHandler] the MCP handler to register tools with + # @param config [Config] the server configuration + def self.register(mcp_handler, config) + register_get_weather(mcp_handler) + register_get_forecast(mcp_handler, config) + register_get_weather_alerts(mcp_handler, config) + end + + # Registers the get-weather tool (no scope required). + # + # @param mcp_handler [McpHandler] the MCP handler + def self.register_get_weather(mcp_handler) + mcp_handler.register_tool( + 'get-weather', + { + description: 'Get current weather for a city. No authentication required.', + inputSchema: { + type: 'object', + properties: { + city: { + type: 'string', + description: 'City name to get weather for', + }, + }, + required: ['city'], + }, + } + ) do |args, _auth_context| + city = args['city'] || 'Unknown' + weather = get_simulated_weather(city) + + { + content: [{ type: 'text', text: JSON.pretty_generate(weather) }], + } + end + end + + # Registers the get-forecast tool (requires mcp:read scope). + # + # @param mcp_handler [McpHandler] the MCP handler + # @param config [Config] the server configuration + def self.register_get_forecast(mcp_handler, config) + mcp_handler.register_tool( + 'get-forecast', + { + description: 'Get 5-day weather forecast for a city. Requires mcp:read scope.', + inputSchema: { + type: 'object', + properties: { + city: { + type: 'string', + description: 'City name to get forecast for', + }, + days: { + type: 'integer', + description: 'Number of days to forecast (default: 5)', + }, + }, + required: ['city'], + }, + } + ) do |args, auth_context| + # Check scope if auth is enabled + if !config.auth_disabled && !auth_context.has_scope?('mcp:read') + next error_result('Access denied. Required scope: mcp:read') + end + + city = args['city'] || 'Unknown' + days = (args['days'] || 5).to_i + forecast = get_simulated_forecast(city, days) + + { + content: [{ type: 'text', text: JSON.pretty_generate({ city: city, forecast: forecast }) }], + } + end + end + + # Registers the get-weather-alerts tool (requires mcp:admin scope). + # + # @param mcp_handler [McpHandler] the MCP handler + # @param config [Config] the server configuration + def self.register_get_weather_alerts(mcp_handler, config) + mcp_handler.register_tool( + 'get-weather-alerts', + { + description: 'Get weather alerts for a region. Requires mcp:admin scope.', + inputSchema: { + type: 'object', + properties: { + region: { + type: 'string', + description: 'Region name to get alerts for', + }, + }, + required: ['region'], + }, + } + ) do |args, auth_context| + # Check scope if auth is enabled + if !config.auth_disabled && !auth_context.has_scope?('mcp:admin') + next error_result('Access denied. Required scope: mcp:admin') + end + + region = args['region'] || 'Unknown' + alerts = get_simulated_alerts(region) + + { + content: [{ type: 'text', text: JSON.pretty_generate({ region: region, alerts: alerts }) }], + } + end + end + + # Creates an error result for access denied or other errors. + # + # @param message [String] the error message + # @return [Hash] error result in MCP format + def self.error_result(message) + { + content: [{ type: 'text', text: JSON.generate({ error: 'access_denied', message: message }) }], + isError: true, + } + end + + # Gets simulated current weather for a city. + # Uses deterministic hash-based simulation. + # + # @param city [String] the city name + # @return [Hash] weather data + def self.get_simulated_weather(city) + hash = city_hash(city) + + { + city: city, + temperature: get_temp_for_city(city), + condition: get_condition_for_city(city), + humidity: 40 + (hash % 40), # 40-80% + windSpeed: 5 + (hash % 25), # 5-30 km/h + } + end + + # Gets simulated weather forecast for a city. + # + # @param city [String] the city name + # @param days [Integer] number of days to forecast (default: 5) + # @return [Array] array of daily forecasts + def self.get_simulated_forecast(city, days = 5) + day_names = ['Today', 'Tomorrow', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7'] + + (0...days).map do |index| + { + day: day_names[index] || "Day #{index + 1}", + high: get_temp_for_city(city, index) + 5, + low: get_temp_for_city(city, index) - 5, + condition: get_condition_for_city(city, index), + } + end + end + + # Gets simulated weather alerts for a region. + # + # @param region [String] the region name + # @return [Array] array of weather alerts + def self.get_simulated_alerts(region) + hash = region.chars.sum(&:ord) + + case hash % 3 + when 0 + [ + { + type: 'Heat Warning', + severity: 'moderate', + message: "High temperatures expected in #{region}. Stay hydrated.", + }, + ] + when 1 + [ + { + type: 'Storm Watch', + severity: 'high', + message: "Severe thunderstorms possible in #{region}. Seek shelter if needed.", + }, + { + type: 'Wind Advisory', + severity: 'low', + message: "Strong winds expected in #{region}. Secure loose objects.", + }, + ] + else + [] # No alerts + end + end + + # Calculates a hash value for a city name. + # + # @param city [String] the city name + # @return [Integer] hash value + def self.city_hash(city) + city.chars.sum(&:ord) + end + + # Gets a deterministic temperature for a city. + # + # @param city [String] the city name + # @param offset [Integer] day offset for forecasts + # @return [Integer] temperature in Celsius (10-35) + def self.get_temp_for_city(city, offset = 0) + hash = city_hash(city) + 10 + ((hash + (offset * 7)) % 26) + end + + # Gets a deterministic condition for a city. + # + # @param city [String] the city name + # @param offset [Integer] day offset for forecasts + # @return [String] weather condition + def self.get_condition_for_city(city, offset = 0) + hash = city_hash(city) + CONDITIONS[(hash + offset) % CONDITIONS.length] + end + end +end diff --git a/examples/auth/run_example.ps1 b/examples/auth/run_example.ps1 new file mode 100644 index 00000000..b66a8f9b --- /dev/null +++ b/examples/auth/run_example.ps1 @@ -0,0 +1,22 @@ +# Run the Ruby Auth MCP Server example +# Usage: .\run_example.ps1 [config_path] + +param( + [string]$ConfigPath = "server.config" +) + +# Change to script directory +Push-Location $PSScriptRoot + +try { + # Install dependencies if needed + Write-Host "Installing dependencies..." + bundle install --quiet + + # Run the server + Write-Host "Starting server with config: $ConfigPath" + bundle exec ruby lib/auth_mcp_server.rb $ConfigPath +} +finally { + Pop-Location +} diff --git a/examples/auth/run_example.sh b/examples/auth/run_example.sh new file mode 100755 index 00000000..1e4cd048 --- /dev/null +++ b/examples/auth/run_example.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Run the Ruby Auth MCP Server example +# Usage: ./run_example.sh [config_path] + +set -e + +# Change to script directory +cd "$(dirname "$0")" + +# Prefer Homebrew Ruby on macOS (system Ruby is too old) +if [[ "$OSTYPE" == "darwin"* ]]; then + if [ -d "/opt/homebrew/opt/ruby/bin" ]; then + export PATH="/opt/homebrew/opt/ruby/bin:$PATH" + elif [ -d "/usr/local/opt/ruby/bin" ]; then + export PATH="/usr/local/opt/ruby/bin:$PATH" + fi +fi + +# Install dependencies if needed +echo "Installing dependencies..." +bundle install --quiet + +# Run the server +CONFIG_PATH="${1:-server.config}" +echo "Starting server with config: $CONFIG_PATH" +bundle exec ruby lib/auth_mcp_server.rb "$CONFIG_PATH" diff --git a/examples/auth/server.config b/examples/auth/server.config new file mode 100644 index 00000000..fc382ea1 --- /dev/null +++ b/examples/auth/server.config @@ -0,0 +1,75 @@ +# Java Auth MCP Server Configuration +# ==================================== +# INI-style configuration file for the OAuth-protected MCP server. +# Lines starting with # are comments. Empty lines are ignored. + +# ============================================================================= +# Server Settings +# ============================================================================= + +# Server bind address +# Use 0.0.0.0 to listen on all interfaces, or 127.0.0.1 for localhost only +host=0.0.0.0 + +# Server port number +port=3001 + +# Public server URL (used in OAuth metadata endpoints) +# If not specified, derived from host and port (with localhost substitution) +server_url=https://marni-nightcapped-nonmeditatively.ngrok-free.dev + +# ============================================================================= +# OAuth/IDP Settings +# ============================================================================= + +# OAuth client credentials +client_id=oauth_0a650b79c5a64c3b920ae8c2b20599d9 +client_secret=6BiU2beUi2wIBxY3MUBLyYqoWKa4t0U_kJVm9mvSOKw +auth_server_url=https://auth-test.gopher.security/realms/gopher-mcp-auth +oauth_authorize_url=https://api-test.gopher.security/oauth/authorize + +# Base URL of the authorization server (e.g., Keycloak realm URL) +# When provided, the following endpoints are automatically derived: +# - jwks_uri: {auth_server_url}/protocol/openid-connect/certs +# - issuer: {auth_server_url} +# - oauth_authorize_url: {auth_server_url}/protocol/openid-connect/auth +# - oauth_token_url: {auth_server_url}/protocol/openid-connect/token + +# Direct OAuth endpoint URLs (optional, override derived values) +# jwks_uri=https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs +# issuer=https://keycloak.example.com/realms/mcp +# oauth_authorize_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth +# oauth_token_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/token + +# ============================================================================= +# Scopes +# ============================================================================= + +# Space-separated list of allowed scopes for token validation +# Tools can require specific scopes for access control +exchange_idps=oauth-idp-714982830194556929-google +allowed_scopes=openid profile email scope-001 + +# ============================================================================= +# Cache Settings +# ============================================================================= + +# JWKS cache duration in seconds (how long to cache the JSON Web Key Set) +jwks_cache_duration=3600 + +# Whether to automatically refresh JWKS before expiration +# Values: true, false, 1, 0 +jwks_auto_refresh=true + +# HTTP request timeout in milliseconds for JWKS fetch and token validation +request_timeout=5000 + +# ============================================================================= +# Development Settings +# ============================================================================= + +# Auth bypass mode - disable authentication for development/testing +# When true, all requests are treated as authenticated with full scopes +# WARNING: Never enable in production! +# Values: true, false, 1, 0 +auth_disabled=false diff --git a/examples/auth/server.config.example b/examples/auth/server.config.example new file mode 100644 index 00000000..05edc74d --- /dev/null +++ b/examples/auth/server.config.example @@ -0,0 +1,34 @@ +# Auth MCP Server Configuration +# Copy this file to server.config and update values as needed + +# Server settings +host=0.0.0.0 +port=3000 +server_url=http://localhost:3000 + +# OAuth/IDP settings +# Uncomment and configure for Keycloak or other OAuth provider +# client_id=your-client-id +# client_secret=your-client-secret +# auth_server_url=https://keycloak.example.com/realms/mcp + +# Direct OAuth endpoint URLs (optional, derived from auth_server_url if not set) +# jwks_uri=https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs +# issuer=https://keycloak.example.com/realms/mcp +# oauth_authorize_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth +# oauth_token_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/token + +# Token exchange IDPs (comma-separated list) +# exchange_idps=google,github + +# Scopes +allowed_scopes=openid profile email mcp:read mcp:write mcp:admin + +# Cache settings +jwks_cache_duration=3600 +jwks_auto_refresh=true +request_timeout=30 + +# Auth bypass mode (for development/testing) +# Set to false to enable authentication +auth_disabled=true diff --git a/examples/auth/spec/config_spec.rb b/examples/auth/spec/config_spec.rb new file mode 100644 index 00000000..c2edc42d --- /dev/null +++ b/examples/auth/spec/config_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require_relative '../lib/config' + +RSpec.describe AuthMcpServer::Config do + describe '#initialize' do + it 'uses default values when no attributes provided' do + config = described_class.new + + expect(config.host).to eq('0.0.0.0') + expect(config.port).to eq(3000) + expect(config.server_url).to eq('http://localhost:3000') + expect(config.auth_disabled).to be true + expect(config.allowed_scopes).to eq('openid profile email') + end + + it 'accepts custom values' do + config = described_class.new( + host: '127.0.0.1', + port: 8080, + server_url: 'https://api.example.com', + auth_disabled: false + ) + + expect(config.host).to eq('127.0.0.1') + expect(config.port).to eq(8080) + expect(config.server_url).to eq('https://api.example.com') + expect(config.auth_disabled).to be false + end + end + + describe '.parse_config_file' do + it 'parses key=value pairs' do + content = <<~CONFIG + host=127.0.0.1 + port=8080 + server_url=https://api.example.com + CONFIG + + attrs = described_class.parse_config_file(content) + + expect(attrs[:host]).to eq('127.0.0.1') + expect(attrs[:port]).to eq(8080) + expect(attrs[:server_url]).to eq('https://api.example.com') + end + + it 'skips comments' do + content = <<~CONFIG + # This is a comment + host=localhost + # Another comment + port=3000 + CONFIG + + attrs = described_class.parse_config_file(content) + + expect(attrs[:host]).to eq('localhost') + expect(attrs[:port]).to eq(3000) + expect(attrs.keys).not_to include(:'#') + end + + it 'skips empty lines' do + content = <<~CONFIG + host=localhost + + port=3000 + + CONFIG + + attrs = described_class.parse_config_file(content) + + expect(attrs.keys.length).to eq(2) + end + + it 'converts boolean values' do + content = <<~CONFIG + auth_disabled=true + jwks_auto_refresh=false + CONFIG + + attrs = described_class.parse_config_file(content) + + expect(attrs[:auth_disabled]).to be true + expect(attrs[:jwks_auto_refresh]).to be false + end + + it 'converts integer values' do + content = <<~CONFIG + port=8080 + jwks_cache_duration=7200 + request_timeout=60 + CONFIG + + attrs = described_class.parse_config_file(content) + + expect(attrs[:port]).to eq(8080) + expect(attrs[:jwks_cache_duration]).to eq(7200) + expect(attrs[:request_timeout]).to eq(60) + end + + it 'handles values with equals signs' do + content = <<~CONFIG + server_url=https://example.com?query=value + CONFIG + + attrs = described_class.parse_config_file(content) + + expect(attrs[:server_url]).to eq('https://example.com?query=value') + end + end + + describe '.load_from_file' do + it 'returns default config when file does not exist' do + config = described_class.load_from_file('/nonexistent/path') + + expect(config.host).to eq('0.0.0.0') + expect(config.port).to eq(3000) + end + end + + describe '#derive_endpoints' do + it 'derives endpoints from auth_server_url' do + config = described_class.new( + auth_server_url: 'https://keycloak.example.com/realms/mcp' + ) + + expect(config.jwks_uri).to eq('https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs') + expect(config.issuer).to eq('https://keycloak.example.com/realms/mcp') + expect(config.oauth_authorize_url).to eq('https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth') + expect(config.oauth_token_url).to eq('https://keycloak.example.com/realms/mcp/protocol/openid-connect/token') + end + + it 'does not override explicitly set endpoints' do + config = described_class.new( + auth_server_url: 'https://keycloak.example.com/realms/mcp', + jwks_uri: 'https://custom.example.com/jwks' + ) + + expect(config.jwks_uri).to eq('https://custom.example.com/jwks') + expect(config.issuer).to eq('https://keycloak.example.com/realms/mcp') + end + + it 'handles trailing slash in auth_server_url' do + config = described_class.new( + auth_server_url: 'https://keycloak.example.com/realms/mcp/' + ) + + expect(config.jwks_uri).to eq('https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs') + end + end + + describe '#auth_enabled?' do + it 'returns false when auth_disabled is true' do + config = described_class.new(auth_disabled: true) + expect(config.auth_enabled?).to be false + end + + it 'returns true when auth_disabled is false' do + config = described_class.new(auth_disabled: false) + expect(config.auth_enabled?).to be true + end + end +end diff --git a/examples/auth/spec/integration_spec.rb b/examples/auth/spec/integration_spec.rb new file mode 100644 index 00000000..25501abd --- /dev/null +++ b/examples/auth/spec/integration_spec.rb @@ -0,0 +1,298 @@ +# frozen_string_literal: true + +require 'json' +require 'rack/test' +require 'sinatra/base' + +# Load all components +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +$LOAD_PATH.unshift(File.expand_path('../../lib', __dir__)) + +require 'gopher_orch/auth/auth_context' +require 'gopher_orch/auth/errors' +require 'gopher_orch/auth/www_authenticate' +require 'gopher_orch/auth/oauth/protected_resource_metadata' +require 'gopher_orch/auth/oauth/authorization_server_metadata' +require 'gopher_orch/auth/oauth/openid_configuration' +require 'gopher_orch/auth/oauth/client_registration_response' +require 'config' +require 'middleware/cors_middleware' +require 'middleware/oauth_auth_middleware' +require 'routes/health' +require 'routes/oauth_endpoints' +require 'routes/mcp_handler' +require 'routes/mcp_endpoints' +require 'tools/weather_tools' + +RSpec.describe 'Integration Tests' do + include Rack::Test::Methods + + let(:config) do + AuthMcpServer::Config.new( + host: '0.0.0.0', + port: 8080, + server_url: 'http://localhost:8080', + auth_server_url: 'http://auth.example.com', + auth_disabled: true, + allowed_scopes: 'openid profile mcp:read mcp:write mcp:admin' + ) + end + + let(:app) do + cfg = config + mcp = AuthMcpServer::McpHandler.new({ name: 'test-server', version: '1.0.0' }) + AuthMcpServer::WeatherTools.register(mcp, cfg) + + Sinatra.new do + use AuthMcpServer::CorsMiddleware + use AuthMcpServer::OAuthAuthMiddleware, cfg + + set :config, cfg + set :mcp_handler, mcp + + register AuthMcpServer::Routes::Health + register AuthMcpServer::Routes::OAuthEndpoints + register AuthMcpServer::Routes::McpEndpoints + end + end + + describe 'Health endpoint' do + it 'returns healthy status' do + get '/health' + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['status']).to eq('ok') + end + end + + describe 'OAuth discovery endpoints' do + it 'returns protected resource metadata' do + get '/.well-known/oauth-protected-resource' + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['resource']).to eq('http://localhost:8080/mcp') + end + + it 'returns authorization server metadata' do + get '/.well-known/oauth-authorization-server' + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['issuer']).to include('example.com') + end + + it 'returns OpenID configuration' do + get '/.well-known/openid-configuration' + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['issuer']).to include('example.com') + expect(body).to have_key('userinfo_endpoint') + end + end + + describe 'Dynamic client registration' do + it 'registers a new client' do + post '/oauth/register', + { redirect_uris: ['http://localhost:3000/callback'] }.to_json, + { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(201) + body = JSON.parse(last_response.body) + expect(body).to have_key('client_id') + end + end + + describe 'MCP initialize flow' do + it 'initializes MCP session' do + request_body = { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: {}, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['result']['protocolVersion']).to eq('2024-11-05') + expect(body['result']['serverInfo']['name']).to eq('test-server') + end + end + + describe 'MCP tools/list flow' do + it 'lists available tools' do + request_body = { + jsonrpc: '2.0', + id: 2, + method: 'tools/list', + params: {}, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + tools = body['result']['tools'] + + expect(tools.length).to eq(3) + tool_names = tools.map { |t| t['name'] } + expect(tool_names).to include('get-weather', 'get-forecast', 'get-weather-alerts') + end + end + + describe 'MCP tools/call flow' do + it 'calls get-weather tool' do + request_body = { + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { + name: 'get-weather', + arguments: { city: 'London' }, + }, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + content = JSON.parse(body['result']['content'][0]['text']) + expect(content['city']).to eq('London') + expect(content).to have_key('temperature') + end + + it 'calls get-forecast tool' do + request_body = { + jsonrpc: '2.0', + id: 4, + method: 'tools/call', + params: { + name: 'get-forecast', + arguments: { city: 'Tokyo', days: 3 }, + }, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + content = JSON.parse(body['result']['content'][0]['text']) + expect(content['city']).to eq('Tokyo') + expect(content['forecast'].length).to eq(3) + end + + it 'calls get-weather-alerts tool' do + request_body = { + jsonrpc: '2.0', + id: 5, + method: 'tools/call', + params: { + name: 'get-weather-alerts', + arguments: { region: 'California' }, + }, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + content = JSON.parse(body['result']['content'][0]['text']) + expect(content['region']).to eq('California') + expect(content).to have_key('alerts') + end + end + + describe 'MCP ping flow' do + it 'responds to ping' do + request_body = { + jsonrpc: '2.0', + id: 6, + method: 'ping', + params: {}, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['result']).to eq({}) + end + end + + describe 'CORS headers' do + it 'includes CORS headers on all responses' do + get '/health' + + expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') + end + + it 'handles OPTIONS preflight' do + options '/mcp' + + expect(last_response.status).to eq(204) + expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') + expect(last_response.headers['Access-Control-Allow-Methods']).to include('POST') + end + end + + describe 'Error handling' do + it 'returns parse error for invalid JSON' do + post '/mcp', '{ broken', { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['error']['code']).to eq(-32_700) + end + + it 'returns method not found for unknown methods' do + request_body = { + jsonrpc: '2.0', + id: 7, + method: 'unknown/method', + params: {}, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['error']['code']).to eq(-32_601) + end + + it 'returns invalid params for missing tool name' do + request_body = { + jsonrpc: '2.0', + id: 8, + method: 'tools/call', + params: {}, + }.to_json + + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['error']['code']).to eq(-32_602) + end + end + + describe '/rpc endpoint alias' do + it 'works same as /mcp' do + request_body = { + jsonrpc: '2.0', + id: 9, + method: 'ping', + params: {}, + }.to_json + + post '/rpc', request_body, { 'CONTENT_TYPE' => 'application/json' } + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body['result']).to eq({}) + end + end +end diff --git a/examples/auth/spec/middleware/cors_middleware_spec.rb b/examples/auth/spec/middleware/cors_middleware_spec.rb new file mode 100644 index 00000000..161e591c --- /dev/null +++ b/examples/auth/spec/middleware/cors_middleware_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require_relative '../../lib/middleware/cors_middleware' + +RSpec.describe AuthMcpServer::Cors do + describe '::STANDARD_HEADERS' do + it 'includes Access-Control-Allow-Origin' do + expect(described_class::STANDARD_HEADERS['Access-Control-Allow-Origin']).to eq('*') + end + + it 'includes Access-Control-Allow-Methods' do + methods = described_class::STANDARD_HEADERS['Access-Control-Allow-Methods'] + expect(methods).to include('GET') + expect(methods).to include('POST') + expect(methods).to include('OPTIONS') + end + + it 'includes MCP-specific headers in Access-Control-Allow-Headers' do + headers = described_class::STANDARD_HEADERS['Access-Control-Allow-Headers'] + expect(headers).to include('Mcp-Session-Id') + expect(headers).to include('Mcp-Protocol-Version') + end + + it 'includes standard headers in Access-Control-Allow-Headers' do + headers = described_class::STANDARD_HEADERS['Access-Control-Allow-Headers'] + expect(headers).to include('Authorization') + expect(headers).to include('Content-Type') + expect(headers).to include('Accept') + end + + it 'exposes WWW-Authenticate header' do + expose = described_class::STANDARD_HEADERS['Access-Control-Expose-Headers'] + expect(expose).to include('WWW-Authenticate') + end + + it 'sets Access-Control-Max-Age' do + expect(described_class::STANDARD_HEADERS['Access-Control-Max-Age']).to eq('86400') + end + end + + describe '.preflight_response' do + it 'returns 204 status' do + status, _headers, _body = described_class.preflight_response + expect(status).to eq(204) + end + + it 'returns CORS headers' do + _status, headers, _body = described_class.preflight_response + expect(headers['Access-Control-Allow-Origin']).to eq('*') + expect(headers['Access-Control-Allow-Methods']).to include('OPTIONS') + end + + it 'returns empty body' do + _status, _headers, body = described_class.preflight_response + expect(body).to eq(['']) + end + + it 'returns a new hash each time' do + _, headers1, = described_class.preflight_response + _, headers2, = described_class.preflight_response + + headers1['X-Custom'] = 'test' + expect(headers2).not_to have_key('X-Custom') + end + end +end + +RSpec.describe AuthMcpServer::CorsMiddleware do + let(:inner_app) do + lambda do |_env| + [200, { 'Content-Type' => 'application/json' }, ['{"status":"ok"}']] + end + end + + let(:middleware) { described_class.new(inner_app) } + + describe '#call' do + context 'with OPTIONS request' do + let(:env) do + { + 'REQUEST_METHOD' => 'OPTIONS', + 'PATH_INFO' => '/mcp', + } + end + + it 'returns 204 status' do + status, _headers, _body = middleware.call(env) + expect(status).to eq(204) + end + + it 'returns CORS headers' do + _status, headers, _body = middleware.call(env) + expect(headers['Access-Control-Allow-Origin']).to eq('*') + expect(headers['Access-Control-Allow-Methods']).to include('POST') + end + + it 'does not call inner app' do + call_count = 0 + counting_app = lambda do |_env| + call_count += 1 + [200, {}, ['ok']] + end + + mw = described_class.new(counting_app) + mw.call(env) + + expect(call_count).to eq(0) + end + end + + context 'with GET request' do + let(:env) do + { + 'REQUEST_METHOD' => 'GET', + 'PATH_INFO' => '/health', + } + end + + it 'calls inner app' do + status, _headers, body = middleware.call(env) + expect(status).to eq(200) + expect(body).to eq(['{"status":"ok"}']) + end + + it 'adds CORS headers to response' do + _status, headers, _body = middleware.call(env) + expect(headers['Access-Control-Allow-Origin']).to eq('*') + expect(headers['Access-Control-Allow-Methods']).to include('GET') + end + + it 'preserves original headers' do + _status, headers, _body = middleware.call(env) + expect(headers['Content-Type']).to eq('application/json') + end + end + + context 'with POST request' do + let(:env) do + { + 'REQUEST_METHOD' => 'POST', + 'PATH_INFO' => '/mcp', + } + end + + it 'calls inner app and adds CORS headers' do + status, headers, _body = middleware.call(env) + expect(status).to eq(200) + expect(headers['Access-Control-Allow-Origin']).to eq('*') + end + end + end +end diff --git a/examples/auth/spec/middleware/oauth_auth_middleware/path_checking_spec.rb b/examples/auth/spec/middleware/oauth_auth_middleware/path_checking_spec.rb new file mode 100644 index 00000000..847add60 --- /dev/null +++ b/examples/auth/spec/middleware/oauth_auth_middleware/path_checking_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/oauth_auth_middleware' + +RSpec.describe AuthMcpServer::OAuthAuthMiddleware do + let(:inner_app) { ->(_env) { [200, {}, ['ok']] } } + let(:config) { AuthMcpServer::Config.new(auth_disabled: true) } + let(:middleware) { described_class.new(inner_app, config) } + + describe 'PUBLIC_PATHS' do + it 'includes /health' do + expect(described_class::PUBLIC_PATHS).to include('/health') + end + + it 'includes /favicon.ico' do + expect(described_class::PUBLIC_PATHS).to include('/favicon.ico') + end + end + + describe 'PUBLIC_PREFIXES' do + it 'includes /.well-known/' do + expect(described_class::PUBLIC_PREFIXES).to include('/.well-known/') + end + + it 'includes /oauth/' do + expect(described_class::PUBLIC_PREFIXES).to include('/oauth/') + end + end + + describe 'PROTECTED_PREFIXES' do + it 'includes /mcp' do + expect(described_class::PROTECTED_PREFIXES).to include('/mcp') + end + + it 'includes /rpc' do + expect(described_class::PROTECTED_PREFIXES).to include('/rpc') + end + + it 'includes /events' do + expect(described_class::PROTECTED_PREFIXES).to include('/events') + end + + it 'includes /sse' do + expect(described_class::PROTECTED_PREFIXES).to include('/sse') + end + end + + describe '#requires_auth?' do + # Access private method for testing + let(:requires_auth) { ->(path) { middleware.send(:requires_auth?, path) } } + + context 'public paths' do + it 'returns false for /health' do + expect(requires_auth.call('/health')).to be false + end + + it 'returns false for /favicon.ico' do + expect(requires_auth.call('/favicon.ico')).to be false + end + end + + context 'public prefixes' do + it 'returns false for /.well-known/oauth-protected-resource' do + expect(requires_auth.call('/.well-known/oauth-protected-resource')).to be false + end + + it 'returns false for /.well-known/openid-configuration' do + expect(requires_auth.call('/.well-known/openid-configuration')).to be false + end + + it 'returns false for /oauth/authorize' do + expect(requires_auth.call('/oauth/authorize')).to be false + end + + it 'returns false for /oauth/register' do + expect(requires_auth.call('/oauth/register')).to be false + end + end + + context 'protected prefixes' do + it 'returns true for /mcp' do + expect(requires_auth.call('/mcp')).to be true + end + + it 'returns true for /mcp/some/path' do + expect(requires_auth.call('/mcp/some/path')).to be true + end + + it 'returns true for /rpc' do + expect(requires_auth.call('/rpc')).to be true + end + + it 'returns true for /events' do + expect(requires_auth.call('/events')).to be true + end + + it 'returns true for /sse' do + expect(requires_auth.call('/sse')).to be true + end + end + + context 'other paths' do + it 'returns false for root path' do + expect(requires_auth.call('/')).to be false + end + + it 'returns false for unknown paths' do + expect(requires_auth.call('/unknown')).to be false + end + end + end + + describe 'auth context for public paths' do + it 'sets empty auth context for /health' do + env = { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/health' } + middleware.call(env) + + expect(env['auth_context']).to be_a(GopherOrch::Auth::AuthContext) + expect(env['auth_context'].authenticated).to be false + end + + it 'sets empty auth context for /.well-known/openid-configuration' do + env = { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/.well-known/openid-configuration' } + middleware.call(env) + + expect(env['auth_context'].authenticated).to be false + end + end + + describe 'auth context when auth_disabled' do + let(:config) { AuthMcpServer::Config.new(auth_disabled: true, allowed_scopes: 'read write') } + + it 'sets anonymous auth context for protected paths' do + env = { 'REQUEST_METHOD' => 'POST', 'PATH_INFO' => '/mcp' } + middleware.call(env) + + expect(env['auth_context'].user_id).to eq('anonymous') + expect(env['auth_context'].authenticated).to be true + expect(env['auth_context'].scopes).to eq('read write') + end + end +end diff --git a/examples/auth/spec/middleware/oauth_auth_middleware/token_extraction_spec.rb b/examples/auth/spec/middleware/oauth_auth_middleware/token_extraction_spec.rb new file mode 100644 index 00000000..2b403d6c --- /dev/null +++ b/examples/auth/spec/middleware/oauth_auth_middleware/token_extraction_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/oauth_auth_middleware' + +RSpec.describe AuthMcpServer::OAuthAuthMiddleware do + let(:inner_app) { ->(_env) { [200, {}, ['ok']] } } + let(:config) { AuthMcpServer::Config.new(auth_disabled: false) } + let(:middleware) { described_class.new(inner_app, config) } + + describe '#extract_token' do + let(:extract_token) { ->(env) { middleware.send(:extract_token, env) } } + + context 'from Authorization header' do + it 'extracts bearer token' do + env = { 'HTTP_AUTHORIZATION' => 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' } + expect(extract_token.call(env)).to eq('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9') + end + + it 'handles token with periods' do + token = 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature' + env = { 'HTTP_AUTHORIZATION' => "Bearer #{token}" } + expect(extract_token.call(env)).to eq(token) + end + + it 'returns nil for non-Bearer authorization' do + env = { 'HTTP_AUTHORIZATION' => 'Basic dXNlcjpwYXNz' } + expect(extract_token.call(env)).to be_nil + end + + it 'returns nil for missing Authorization header' do + env = {} + expect(extract_token.call(env)).to be_nil + end + + it 'returns nil for empty Authorization header' do + env = { 'HTTP_AUTHORIZATION' => '' } + expect(extract_token.call(env)).to be_nil + end + end + + context 'from query parameter' do + it 'extracts access_token from query string' do + env = { 'QUERY_STRING' => 'access_token=my-token-123' } + expect(extract_token.call(env)).to eq('my-token-123') + end + + it 'extracts access_token with other parameters' do + env = { 'QUERY_STRING' => 'foo=bar&access_token=my-token-456&baz=qux' } + expect(extract_token.call(env)).to eq('my-token-456') + end + + it 'handles URL-encoded tokens' do + env = { 'QUERY_STRING' => 'access_token=token%2Bwith%2Bplus' } + expect(extract_token.call(env)).to eq('token+with+plus') + end + + it 'returns nil for empty query string' do + env = { 'QUERY_STRING' => '' } + expect(extract_token.call(env)).to be_nil + end + + it 'returns nil when access_token not present' do + env = { 'QUERY_STRING' => 'other_param=value' } + expect(extract_token.call(env)).to be_nil + end + end + + context 'priority' do + it 'prefers Authorization header over query parameter' do + env = { + 'HTTP_AUTHORIZATION' => 'Bearer header-token', + 'QUERY_STRING' => 'access_token=query-token', + } + expect(extract_token.call(env)).to eq('header-token') + end + + it 'falls back to query parameter when no Authorization header' do + env = { 'QUERY_STRING' => 'access_token=query-token' } + expect(extract_token.call(env)).to eq('query-token') + end + end + end + + describe 'missing token handling' do + it 'returns 401 when token missing on protected path' do + env = { + 'REQUEST_METHOD' => 'POST', + 'PATH_INFO' => '/mcp', + 'QUERY_STRING' => '', + } + + status, _headers, _body = middleware.call(env) + expect(status).to eq(401) + end + + it 'includes error in response body' do + env = { + 'REQUEST_METHOD' => 'POST', + 'PATH_INFO' => '/mcp', + 'QUERY_STRING' => '', + } + + _status, _headers, body = middleware.call(env) + response = JSON.parse(body.first) + + expect(response['error']).to eq('invalid_request') + expect(response['error_description']).to eq('Missing bearer token') + end + end +end diff --git a/examples/auth/spec/middleware/oauth_auth_middleware/unauthorized_response_spec.rb b/examples/auth/spec/middleware/oauth_auth_middleware/unauthorized_response_spec.rb new file mode 100644 index 00000000..9f1e43b3 --- /dev/null +++ b/examples/auth/spec/middleware/oauth_auth_middleware/unauthorized_response_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/oauth_auth_middleware' + +RSpec.describe AuthMcpServer::OAuthAuthMiddleware do + let(:inner_app) { ->(_env) { [200, {}, ['ok']] } } + let(:config) do + AuthMcpServer::Config.new( + auth_disabled: false, + server_url: 'https://api.example.com', + allowed_scopes: 'openid profile mcp:read mcp:write' + ) + end + let(:middleware) { described_class.new(inner_app, config) } + + describe '#send_unauthorized' do + let(:send_unauthorized) do + ->(error, description) { middleware.send(:send_unauthorized, error, description) } + end + + it 'returns 401 status' do + status, _headers, _body = send_unauthorized.call('invalid_request', 'Missing token') + expect(status).to eq(401) + end + + describe 'WWW-Authenticate header' do + it 'includes WWW-Authenticate header' do + _status, headers, _body = send_unauthorized.call('invalid_token', 'Token expired') + expect(headers).to have_key('WWW-Authenticate') + end + + it 'starts with Bearer scheme' do + _status, headers, _body = send_unauthorized.call('invalid_token', 'Token expired') + expect(headers['WWW-Authenticate']).to start_with('Bearer ') + end + + it 'includes realm from server_url' do + _status, headers, _body = send_unauthorized.call('invalid_token', 'Token expired') + expect(headers['WWW-Authenticate']).to include('realm="https://api.example.com"') + end + + it 'includes resource_metadata URL' do + _status, headers, _body = send_unauthorized.call('invalid_token', 'Token expired') + expect(headers['WWW-Authenticate']).to include( + 'resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"' + ) + end + + it 'includes scopes from config' do + _status, headers, _body = send_unauthorized.call('invalid_token', 'Token expired') + expect(headers['WWW-Authenticate']).to include('scope="openid profile mcp:read mcp:write"') + end + + it 'includes error code' do + _status, headers, _body = send_unauthorized.call('invalid_token', 'Token expired') + expect(headers['WWW-Authenticate']).to include('error="invalid_token"') + end + + it 'includes error description' do + _status, headers, _body = send_unauthorized.call('invalid_token', 'The access token has expired') + expect(headers['WWW-Authenticate']).to include('error_description="The access token has expired"') + end + end + + describe 'response headers' do + it 'includes Content-Type application/json' do + _status, headers, _body = send_unauthorized.call('invalid_request', 'Missing token') + expect(headers['Content-Type']).to eq('application/json') + end + + it 'includes CORS Allow-Origin header' do + _status, headers, _body = send_unauthorized.call('invalid_request', 'Missing token') + expect(headers['Access-Control-Allow-Origin']).to eq('*') + end + + it 'exposes WWW-Authenticate in CORS headers' do + _status, headers, _body = send_unauthorized.call('invalid_request', 'Missing token') + expect(headers['Access-Control-Expose-Headers']).to include('WWW-Authenticate') + end + + it 'includes Access-Control-Allow-Methods' do + _status, headers, _body = send_unauthorized.call('invalid_request', 'Missing token') + expect(headers['Access-Control-Allow-Methods']).to include('POST') + end + + it 'includes Access-Control-Allow-Headers' do + _status, headers, _body = send_unauthorized.call('invalid_request', 'Missing token') + expect(headers['Access-Control-Allow-Headers']).to include('Authorization') + end + end + + describe 'response body' do + it 'returns JSON body' do + _status, _headers, body = send_unauthorized.call('invalid_request', 'Missing token') + expect { JSON.parse(body.first) }.not_to raise_error + end + + it 'includes error in body' do + _status, _headers, body = send_unauthorized.call('invalid_request', 'Missing token') + parsed = JSON.parse(body.first) + expect(parsed['error']).to eq('invalid_request') + end + + it 'includes error_description in body' do + _status, _headers, body = send_unauthorized.call('invalid_request', 'Missing bearer token') + parsed = JSON.parse(body.first) + expect(parsed['error_description']).to eq('Missing bearer token') + end + end + end + + describe 'unauthorized response in middleware flow' do + it 'returns 401 for protected path without token' do + env = { + 'REQUEST_METHOD' => 'POST', + 'PATH_INFO' => '/mcp', + 'QUERY_STRING' => '', + } + + status, headers, = middleware.call(env) + + expect(status).to eq(401) + expect(headers['WWW-Authenticate']).to start_with('Bearer ') + end + + it 'includes proper error for missing token' do + env = { + 'REQUEST_METHOD' => 'POST', + 'PATH_INFO' => '/mcp', + 'QUERY_STRING' => '', + } + + _status, headers, body = middleware.call(env) + parsed = JSON.parse(body.first) + + expect(parsed['error']).to eq('invalid_request') + expect(parsed['error_description']).to eq('Missing bearer token') + expect(headers['WWW-Authenticate']).to include('error="invalid_request"') + end + end + + describe 'OAuth error codes' do + it 'handles invalid_request error' do + _status, headers, _body = middleware.send(:send_unauthorized, 'invalid_request', 'Bad request') + expect(headers['WWW-Authenticate']).to include('error="invalid_request"') + end + + it 'handles invalid_token error' do + _status, headers, _body = middleware.send(:send_unauthorized, 'invalid_token', 'Token invalid') + expect(headers['WWW-Authenticate']).to include('error="invalid_token"') + end + + it 'handles insufficient_scope error' do + _status, headers, _body = middleware.send(:send_unauthorized, 'insufficient_scope', 'Need admin') + expect(headers['WWW-Authenticate']).to include('error="insufficient_scope"') + end + end +end diff --git a/examples/auth/spec/routes/health_spec.rb b/examples/auth/spec/routes/health_spec.rb new file mode 100644 index 00000000..9f6840f6 --- /dev/null +++ b/examples/auth/spec/routes/health_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'json' +require 'time' +require_relative '../../lib/auth_mcp_server' +require_relative '../../lib/middleware/cors_middleware' +require_relative '../../lib/routes/health' + +# Mock Sinatra application for testing +class MockHealthApp + attr_reader :routes + + def initialize + @routes = { get: {}, options: {} } + end + + def get(path, &block) + @routes[:get][path] = block + end + + def options(path, &block) + @routes[:options][path] = block + end + + def call_route(method, path) + @current_response = MockResponse.new + block = @routes[method][path] + return nil unless block + + result = instance_eval(&block) + [@current_response.status_code, @current_response.headers, [result]] + end + + def content_type(type) + @current_response.headers['Content-Type'] = type == :json ? 'application/json' : type.to_s + end + + def status(code) + @current_response.status_code = code + end + + def response + @current_response + end +end + +class MockResponse + attr_accessor :status_code, :headers + + def initialize + @status_code = 200 + @headers = {} + end +end + +RSpec.describe AuthMcpServer::Routes::Health do + let(:app) { MockHealthApp.new } + + before do + described_class.registered(app) + end + + describe 'GET /health' do + it 'registers the route' do + expect(app.routes[:get]).to have_key('/health') + end + + it 'returns JSON response' do + _status, headers, body = app.call_route(:get, '/health') + expect(headers['Content-Type']).to eq('application/json') + + response = JSON.parse(body.first) + expect(response).to have_key('status') + expect(response).to have_key('timestamp') + expect(response).to have_key('version') + end + + it 'returns status ok' do + _status, _headers, body = app.call_route(:get, '/health') + response = JSON.parse(body.first) + expect(response['status']).to eq('ok') + end + + it 'returns ISO8601 timestamp' do + _status, _headers, body = app.call_route(:get, '/health') + response = JSON.parse(body.first) + + # Should not raise when parsing + timestamp = Time.iso8601(response['timestamp']) + expect(timestamp).to be_a(Time) + end + + it 'returns version' do + _status, _headers, body = app.call_route(:get, '/health') + response = JSON.parse(body.first) + expect(response['version']).to eq(AuthMcpServer::VERSION) + end + end + + describe 'OPTIONS /health' do + it 'registers the route' do + expect(app.routes[:options]).to have_key('/health') + end + end +end diff --git a/examples/auth/spec/routes/mcp_endpoints_spec.rb b/examples/auth/spec/routes/mcp_endpoints_spec.rb new file mode 100644 index 00000000..fc1ebf78 --- /dev/null +++ b/examples/auth/spec/routes/mcp_endpoints_spec.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +require 'json' +require 'rack/test' +require 'sinatra/base' +require_relative '../../lib/middleware/cors_middleware' +require_relative '../../lib/routes/mcp_handler' +require_relative '../../lib/routes/mcp_endpoints' + +RSpec.describe AuthMcpServer::Routes::McpEndpoints do + include Rack::Test::Methods + + let(:mcp_handler) do + handler = AuthMcpServer::McpHandler.new({ name: 'test-server', version: '1.0.0' }) + handler.register_tool('echo', { description: 'Echo message' }) do |args, _ctx| + { content: [{ type: 'text', text: args['message'] }] } + end + handler + end + + let(:mock_auth_context) do + Class.new do + def user_id + 'test-user' + end + + def scopes + 'read write' + end + + def authenticated + true + end + end.new + end + + let(:app) do + handler = mcp_handler + auth_ctx = mock_auth_context + + Sinatra.new do + register AuthMcpServer::Routes::McpEndpoints + set :mcp_handler, handler + + before do + env['auth_context'] = auth_ctx + end + end + end + + describe 'OPTIONS /mcp' do + before { options '/mcp' } + + it 'returns 204 status' do + expect(last_response.status).to eq(204) + end + + it 'includes CORS headers' do + expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') + end + + it 'includes Access-Control-Allow-Methods' do + expect(last_response.headers['Access-Control-Allow-Methods']).to include('POST') + end + end + + describe 'OPTIONS /rpc' do + before { options '/rpc' } + + it 'returns 204 status' do + expect(last_response.status).to eq(204) + end + + it 'includes CORS headers' do + expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') + end + end + + describe 'POST /mcp' do + context 'with valid initialize request' do + let(:request_body) do + { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }.to_json + end + + before do + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + end + + it 'returns 200 status' do + expect(last_response.status).to eq(200) + end + + it 'returns JSON content type' do + expect(last_response.content_type).to include('application/json') + end + + it 'returns JSON-RPC response' do + body = JSON.parse(last_response.body) + expect(body['jsonrpc']).to eq('2.0') + expect(body['id']).to eq(1) + end + + it 'returns initialize result' do + body = JSON.parse(last_response.body) + expect(body['result']['protocolVersion']).to eq('2024-11-05') + expect(body['result']['serverInfo']['name']).to eq('test-server') + end + + it 'includes CORS headers' do + expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') + end + end + + context 'with tools/list request' do + let(:request_body) do + { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }.to_json + end + + before do + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + end + + it 'returns tools array' do + body = JSON.parse(last_response.body) + expect(body['result']['tools']).to be_an(Array) + expect(body['result']['tools'].length).to eq(1) + expect(body['result']['tools'][0]['name']).to eq('echo') + end + end + + context 'with tools/call request' do + let(:request_body) do + { + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { name: 'echo', arguments: { message: 'hello' } }, + }.to_json + end + + before do + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + end + + it 'executes the tool' do + body = JSON.parse(last_response.body) + expect(body['result']['content'][0]['text']).to eq('hello') + end + end + + context 'with ping request' do + let(:request_body) do + { jsonrpc: '2.0', id: 4, method: 'ping', params: {} }.to_json + end + + before do + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + end + + it 'returns empty result' do + body = JSON.parse(last_response.body) + expect(body['result']).to eq({}) + end + end + + context 'with invalid JSON' do + before do + post '/mcp', '{ broken json', { 'CONTENT_TYPE' => 'application/json' } + end + + it 'returns 200 status (JSON-RPC error in body)' do + expect(last_response.status).to eq(200) + end + + it 'returns parse error' do + body = JSON.parse(last_response.body) + expect(body['error']['code']).to eq(-32_700) + end + end + + context 'with unknown method' do + let(:request_body) do + { jsonrpc: '2.0', id: 5, method: 'unknown/method', params: {} }.to_json + end + + before do + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + end + + it 'returns method not found error' do + body = JSON.parse(last_response.body) + expect(body['error']['code']).to eq(-32_601) + end + end + end + + describe 'POST /rpc' do + context 'with valid request' do + let(:request_body) do + { jsonrpc: '2.0', id: 1, method: 'ping', params: {} }.to_json + end + + before do + post '/rpc', request_body, { 'CONTENT_TYPE' => 'application/json' } + end + + it 'returns 200 status' do + expect(last_response.status).to eq(200) + end + + it 'handles requests same as /mcp' do + body = JSON.parse(last_response.body) + expect(body['jsonrpc']).to eq('2.0') + expect(body['result']).to eq({}) + end + + it 'includes CORS headers' do + expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') + end + end + end + + describe 'response headers' do + let(:request_body) do + { jsonrpc: '2.0', id: 1, method: 'ping', params: {} }.to_json + end + + before do + post '/mcp', request_body, { 'CONTENT_TYPE' => 'application/json' } + end + + it 'sets Content-Type to application/json' do + expect(last_response.content_type).to include('application/json') + end + + it 'includes Access-Control-Allow-Origin' do + expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') + end + + it 'includes Access-Control-Allow-Methods' do + expect(last_response.headers['Access-Control-Allow-Methods']).to include('POST') + end + + it 'includes Access-Control-Allow-Headers' do + headers = last_response.headers['Access-Control-Allow-Headers'] + expect(headers).to include('Authorization') + expect(headers).to include('Content-Type') + end + + it 'includes Access-Control-Expose-Headers' do + expect(last_response.headers['Access-Control-Expose-Headers']).to include('WWW-Authenticate') + end + end +end diff --git a/examples/auth/spec/routes/mcp_handler/json_rpc_spec.rb b/examples/auth/spec/routes/mcp_handler/json_rpc_spec.rb new file mode 100644 index 00000000..7d28b9f3 --- /dev/null +++ b/examples/auth/spec/routes/mcp_handler/json_rpc_spec.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/routes/mcp_handler' + +RSpec.describe AuthMcpServer::JsonRpcErrorCodes do + it 'defines PARSE_ERROR as -32700' do + expect(described_class::PARSE_ERROR).to eq(-32_700) + end + + it 'defines INVALID_REQUEST as -32600' do + expect(described_class::INVALID_REQUEST).to eq(-32_600) + end + + it 'defines METHOD_NOT_FOUND as -32601' do + expect(described_class::METHOD_NOT_FOUND).to eq(-32_601) + end + + it 'defines INVALID_PARAMS as -32602' do + expect(described_class::INVALID_PARAMS).to eq(-32_602) + end + + it 'defines INTERNAL_ERROR as -32603' do + expect(described_class::INTERNAL_ERROR).to eq(-32_603) + end +end + +RSpec.describe AuthMcpServer::JsonRpcError do + describe '#initialize' do + it 'creates error with code and message' do + error = described_class.new(-32_600, 'Invalid Request') + + expect(error.code).to eq(-32_600) + expect(error.message).to eq('Invalid Request') + expect(error.data).to be_nil + end + + it 'creates error with optional data' do + error = described_class.new(-32_700, 'Parse error', 'unexpected token') + + expect(error.code).to eq(-32_700) + expect(error.message).to eq('Parse error') + expect(error.data).to eq('unexpected token') + end + end + + describe '#to_h' do + it 'returns hash with code and message' do + error = described_class.new(-32_600, 'Invalid Request') + hash = error.to_h + + expect(hash[:code]).to eq(-32_600) + expect(hash[:message]).to eq('Invalid Request') + expect(hash).not_to have_key(:data) + end + + it 'includes data when present' do + error = described_class.new(-32_700, 'Parse error', 'details') + hash = error.to_h + + expect(hash[:data]).to eq('details') + end + end + + it 'is a StandardError' do + error = described_class.new(-32_600, 'Invalid Request') + expect(error).to be_a(StandardError) + end +end + +RSpec.describe AuthMcpServer::McpHandler do + let(:server_info) { { name: 'test-server', version: '1.0.0' } } + let(:handler) { described_class.new(server_info) } + + describe '#parse_request' do + context 'with valid request' do + it 'parses valid JSON-RPC 2.0 request' do + body = { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }.to_json + result = handler.parse_request(body) + + expect(result[:id]).to eq(1) + expect(result[:method]).to eq('initialize') + expect(result[:params]).to eq({}) + end + + it 'handles string id' do + body = { jsonrpc: '2.0', id: 'abc-123', method: 'ping' }.to_json + result = handler.parse_request(body) + + expect(result[:id]).to eq('abc-123') + end + + it 'handles null id (notification)' do + body = { jsonrpc: '2.0', id: nil, method: 'ping' }.to_json + result = handler.parse_request(body) + + expect(result[:id]).to be_nil + end + + it 'defaults params to empty hash' do + body = { jsonrpc: '2.0', id: 1, method: 'ping' }.to_json + result = handler.parse_request(body) + + expect(result[:params]).to eq({}) + end + end + + context 'with invalid JSON' do + it 'raises PARSE_ERROR for malformed JSON' do + body = '{ invalid json }' + + expect { handler.parse_request(body) }.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::PARSE_ERROR) + expect(error.message).to include('Parse error') + end + end + + it 'includes parse error details in data' do + body = '{ broken' + + expect { handler.parse_request(body) }.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.data).not_to be_nil + end + end + end + + context 'with missing jsonrpc version' do + it 'raises INVALID_REQUEST' do + body = { id: 1, method: 'ping' }.to_json + + expect { handler.parse_request(body) }.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::INVALID_REQUEST) + end + end + end + + context 'with wrong jsonrpc version' do + it 'raises INVALID_REQUEST for version 1.0' do + body = { jsonrpc: '1.0', id: 1, method: 'ping' }.to_json + + expect { handler.parse_request(body) }.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::INVALID_REQUEST) + end + end + end + + context 'with missing method' do + it 'raises INVALID_REQUEST' do + body = { jsonrpc: '2.0', id: 1 }.to_json + + expect { handler.parse_request(body) }.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::INVALID_REQUEST) + expect(error.message).to include('method') + end + end + end + + context 'with empty method' do + it 'raises INVALID_REQUEST' do + body = { jsonrpc: '2.0', id: 1, method: '' }.to_json + + expect { handler.parse_request(body) }.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::INVALID_REQUEST) + end + end + end + end + + describe '#build_response' do + it 'builds success response' do + response = handler.build_response(1, { status: 'ok' }) + + expect(response[:jsonrpc]).to eq('2.0') + expect(response[:id]).to eq(1) + expect(response[:result]).to eq({ status: 'ok' }) + end + + it 'preserves string id' do + response = handler.build_response('request-123', {}) + + expect(response[:id]).to eq('request-123') + end + end + + describe '#build_error_response' do + it 'builds error response from JsonRpcError' do + error = AuthMcpServer::JsonRpcError.new(-32_600, 'Invalid Request') + response = handler.build_error_response(1, error) + + expect(response[:jsonrpc]).to eq('2.0') + expect(response[:id]).to eq(1) + expect(response[:error][:code]).to eq(-32_600) + expect(response[:error][:message]).to eq('Invalid Request') + end + + it 'builds error response from hash' do + error_hash = { code: -32_601, message: 'Method not found' } + response = handler.build_error_response(1, error_hash) + + expect(response[:error]).to eq(error_hash) + end + end +end diff --git a/examples/auth/spec/routes/mcp_handler/methods_spec.rb b/examples/auth/spec/routes/mcp_handler/methods_spec.rb new file mode 100644 index 00000000..112845c7 --- /dev/null +++ b/examples/auth/spec/routes/mcp_handler/methods_spec.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/routes/mcp_handler' + +RSpec.describe AuthMcpServer::McpHandler do + let(:server_info) { { name: 'test-server', version: '1.0.0' } } + let(:handler) { described_class.new(server_info) } + let(:mock_auth_context) do + double('AuthContext', user_id: 'test-user', scopes: 'read write', authenticated: true) + end + + describe 'handle_initialize' do + let(:result) { handler.dispatch_method('initialize', {}, mock_auth_context) } + + it 'returns protocolVersion' do + expect(result[:protocolVersion]).to eq('2024-11-05') + end + + it 'returns capabilities with tools' do + expect(result[:capabilities]).to eq({ tools: {} }) + end + + it 'returns serverInfo' do + expect(result[:serverInfo]).to eq(server_info) + end + + it 'includes all required fields' do + expect(result).to have_key(:protocolVersion) + expect(result).to have_key(:capabilities) + expect(result).to have_key(:serverInfo) + end + end + + describe 'handle_tools_list' do + context 'with no tools registered' do + it 'returns empty tools array' do + result = handler.dispatch_method('tools/list', {}, mock_auth_context) + expect(result[:tools]).to eq([]) + end + end + + context 'with tools registered' do + before do + handler.register_tool('tool-1', { description: 'Tool 1' }) { |_a, _c| {} } + handler.register_tool('tool-2', { description: 'Tool 2' }) { |_a, _c| {} } + end + + it 'returns all registered tools' do + result = handler.dispatch_method('tools/list', {}, mock_auth_context) + expect(result[:tools].length).to eq(2) + end + + it 'includes tool names' do + result = handler.dispatch_method('tools/list', {}, mock_auth_context) + names = result[:tools].map { |t| t[:name] } + expect(names).to contain_exactly('tool-1', 'tool-2') + end + + it 'includes tool descriptions' do + result = handler.dispatch_method('tools/list', {}, mock_auth_context) + descriptions = result[:tools].map { |t| t[:description] } + expect(descriptions).to contain_exactly('Tool 1', 'Tool 2') + end + end + end + + describe 'handle_tools_call' do + context 'with missing name parameter' do + it 'raises INVALID_PARAMS error' do + expect do + handler.dispatch_method('tools/call', {}, mock_auth_context) + end.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::INVALID_PARAMS) + end + end + + it 'includes descriptive error message' do + expect do + handler.dispatch_method('tools/call', {}, mock_auth_context) + end.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.message).to include('name') + end + end + end + + context 'with non-existent tool' do + it 'raises METHOD_NOT_FOUND error' do + expect do + handler.dispatch_method('tools/call', { 'name' => 'unknown-tool' }, mock_auth_context) + end.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::METHOD_NOT_FOUND) + end + end + + it 'includes tool name in error message' do + expect do + handler.dispatch_method('tools/call', { 'name' => 'unknown-tool' }, mock_auth_context) + end.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.message).to include('unknown-tool') + end + end + end + + context 'with valid tool' do + before do + handler.register_tool('echo', { description: 'Echo tool' }) do |args, ctx| + { echoed: args['message'], user: ctx.user_id } + end + end + + it 'executes the tool handler' do + result = handler.dispatch_method( + 'tools/call', + { 'name' => 'echo', 'arguments' => { 'message' => 'hello' } }, + mock_auth_context + ) + expect(result[:echoed]).to eq('hello') + end + + it 'passes auth context to handler' do + result = handler.dispatch_method( + 'tools/call', + { 'name' => 'echo', 'arguments' => { 'message' => 'test' } }, + mock_auth_context + ) + expect(result[:user]).to eq('test-user') + end + + it 'handles missing arguments with empty hash' do + handler.register_tool('no-args', { description: 'No args' }) do |args, _ctx| + { args_count: args.keys.length } + end + + result = handler.dispatch_method( + 'tools/call', + { 'name' => 'no-args' }, + mock_auth_context + ) + expect(result[:args_count]).to eq(0) + end + end + end + + describe 'handle_ping' do + it 'returns empty hash' do + result = handler.dispatch_method('ping', {}, mock_auth_context) + expect(result).to eq({}) + end + end + + describe 'full request/response cycle' do + before do + handler.register_tool('add', { + description: 'Add two numbers', + inputSchema: { + type: 'object', + properties: { + a: { type: 'number' }, + b: { type: 'number' }, + }, + required: %w[a b], + }, + }) do |args, _ctx| + { sum: args['a'].to_i + args['b'].to_i } + end + end + + it 'handles initialize request' do + body = { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:jsonrpc]).to eq('2.0') + expect(response[:id]).to eq(1) + expect(response[:result][:protocolVersion]).to eq('2024-11-05') + end + + it 'handles tools/list request' do + body = { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:result][:tools].length).to eq(1) + expect(response[:result][:tools].first[:name]).to eq('add') + end + + it 'handles tools/call request' do + body = { + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { name: 'add', arguments: { a: 5, b: 3 } }, + }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:result][:sum]).to eq(8) + end + + it 'handles ping request' do + body = { jsonrpc: '2.0', id: 4, method: 'ping', params: {} }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:result]).to eq({}) + end + + it 'returns error for missing tool name' do + body = { jsonrpc: '2.0', id: 5, method: 'tools/call', params: {} }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:error][:code]).to eq(AuthMcpServer::JsonRpcErrorCodes::INVALID_PARAMS) + end + + it 'returns error for unknown tool' do + body = { + jsonrpc: '2.0', + id: 6, + method: 'tools/call', + params: { name: 'nonexistent' }, + }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:error][:code]).to eq(AuthMcpServer::JsonRpcErrorCodes::METHOD_NOT_FOUND) + end + end +end diff --git a/examples/auth/spec/routes/mcp_handler/tool_registration_spec.rb b/examples/auth/spec/routes/mcp_handler/tool_registration_spec.rb new file mode 100644 index 00000000..fc9587cb --- /dev/null +++ b/examples/auth/spec/routes/mcp_handler/tool_registration_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/routes/mcp_handler' + +RSpec.describe AuthMcpServer::McpHandler do + let(:server_info) { { name: 'test-server', version: '1.0.0' } } + let(:handler) { described_class.new(server_info) } + + describe '#register_tool' do + it 'registers a tool with spec and handler' do + handler.register_tool('test-tool', { description: 'A test tool' }) do |_args, _ctx| + { result: 'ok' } + end + + tools = handler.get_tools + expect(tools.length).to eq(1) + expect(tools.first[:name]).to eq('test-tool') + end + + it 'registers multiple tools' do + handler.register_tool('tool-1', { description: 'Tool 1' }) { |_a, _c| {} } + handler.register_tool('tool-2', { description: 'Tool 2' }) { |_a, _c| {} } + handler.register_tool('tool-3', { description: 'Tool 3' }) { |_a, _c| {} } + + tools = handler.get_tools + expect(tools.length).to eq(3) + end + end + + describe '#get_tools' do + it 'returns empty array when no tools registered' do + expect(handler.get_tools).to eq([]) + end + + it 'includes name in tool spec' do + handler.register_tool('my-tool', { description: 'My tool' }) { |_a, _c| {} } + + tools = handler.get_tools + expect(tools.first[:name]).to eq('my-tool') + end + + it 'includes description in tool spec' do + handler.register_tool('my-tool', { description: 'A helpful tool' }) { |_a, _c| {} } + + tools = handler.get_tools + expect(tools.first[:description]).to eq('A helpful tool') + end + + it 'includes inputSchema in tool spec' do + input_schema = { + type: 'object', + properties: { + city: { type: 'string', description: 'City name' }, + }, + required: ['city'], + } + + handler.register_tool('weather', { description: 'Get weather', inputSchema: input_schema }) { |_a, _c| {} } + + tools = handler.get_tools + expect(tools.first[:inputSchema]).to eq(input_schema) + end + end + + describe '#handle_request' do + let(:mock_auth_context) do + double('AuthContext', user_id: 'test-user', scopes: 'read write', authenticated: true) + end + + it 'parses request and returns response' do + body = { jsonrpc: '2.0', id: 1, method: 'ping' }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:jsonrpc]).to eq('2.0') + expect(response[:id]).to eq(1) + expect(response).to have_key(:result) + end + + it 'returns error response for invalid JSON' do + body = '{ broken' + response = handler.handle_request(body, mock_auth_context) + + expect(response[:error][:code]).to eq(AuthMcpServer::JsonRpcErrorCodes::PARSE_ERROR) + end + + it 'returns error response for unknown method' do + body = { jsonrpc: '2.0', id: 1, method: 'unknown/method' }.to_json + response = handler.handle_request(body, mock_auth_context) + + expect(response[:error][:code]).to eq(AuthMcpServer::JsonRpcErrorCodes::METHOD_NOT_FOUND) + end + end + + describe '#dispatch_method' do + let(:mock_auth_context) do + double('AuthContext', user_id: 'test-user', scopes: 'read write', authenticated: true) + end + + it 'dispatches initialize method' do + expect { handler.dispatch_method('initialize', {}, mock_auth_context) }.not_to raise_error + end + + it 'dispatches tools/list method' do + expect { handler.dispatch_method('tools/list', {}, mock_auth_context) }.not_to raise_error + end + + it 'dispatches tools/call method' do + handler.register_tool('test-tool', { description: 'Test' }) { |_a, _c| { result: 'ok' } } + params = { 'name' => 'test-tool', 'arguments' => {} } + expect { handler.dispatch_method('tools/call', params, mock_auth_context) }.not_to raise_error + end + + it 'dispatches ping method' do + expect { handler.dispatch_method('ping', {}, mock_auth_context) }.not_to raise_error + end + + it 'raises METHOD_NOT_FOUND for unknown methods' do + expect do + handler.dispatch_method('unknown/method', {}, mock_auth_context) + end.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.code).to eq(AuthMcpServer::JsonRpcErrorCodes::METHOD_NOT_FOUND) + end + end + + it 'includes method name in error message' do + expect do + handler.dispatch_method('foo/bar', {}, mock_auth_context) + end.to raise_error(AuthMcpServer::JsonRpcError) do |error| + expect(error.message).to include('foo/bar') + end + end + end +end diff --git a/examples/auth/spec/routes/oauth_endpoints/auth_server_metadata_spec.rb b/examples/auth/spec/routes/oauth_endpoints/auth_server_metadata_spec.rb new file mode 100644 index 00000000..a04214f2 --- /dev/null +++ b/examples/auth/spec/routes/oauth_endpoints/auth_server_metadata_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/cors_middleware' +require_relative '../../../lib/routes/oauth_endpoints' + +RSpec.describe 'Authorization Server Metadata Endpoint' do + let(:config) do + AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + auth_server_url: 'https://auth.example.com/realms/mcp', + allowed_scopes: 'openid profile email mcp:read' + ) + end + + let(:helper_instance) do + Class.new do + include AuthMcpServer::Routes::OAuthEndpoints::Helpers + end.new + end + + describe 'build_authorization_server_metadata' do + it 'uses issuer from config when set' do + config_with_issuer = AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + issuer: 'https://custom-issuer.example.com' + ) + + metadata = helper_instance.build_authorization_server_metadata(config_with_issuer) + expect(metadata.issuer).to eq('https://custom-issuer.example.com') + end + + it 'falls back to server_url for issuer' do + config_no_issuer = AuthMcpServer::Config.new( + server_url: 'https://api.example.com' + ) + + metadata = helper_instance.build_authorization_server_metadata(config_no_issuer) + expect(metadata.issuer).to eq('https://api.example.com') + end + + it 'derives authorization endpoint from auth_server_url' do + metadata = helper_instance.build_authorization_server_metadata(config) + expect(metadata.authorization_endpoint).to eq('https://auth.example.com/realms/mcp/protocol/openid-connect/auth') + end + + it 'derives token endpoint from auth_server_url' do + metadata = helper_instance.build_authorization_server_metadata(config) + expect(metadata.token_endpoint).to eq('https://auth.example.com/realms/mcp/protocol/openid-connect/token') + end + + it 'includes registration endpoint' do + metadata = helper_instance.build_authorization_server_metadata(config) + expect(metadata.registration_endpoint).to eq('https://api.example.com/oauth/register') + end + + it 'includes scopes from config' do + metadata = helper_instance.build_authorization_server_metadata(config) + expect(metadata.scopes_supported).to eq(%w[openid profile email mcp:read]) + end + + it 'includes standard response types' do + metadata = helper_instance.build_authorization_server_metadata(config) + expect(metadata.response_types_supported).to eq(['code']) + end + + it 'includes standard grant types' do + metadata = helper_instance.build_authorization_server_metadata(config) + expect(metadata.grant_types_supported).to eq(%w[authorization_code refresh_token]) + end + + it 'includes PKCE support' do + metadata = helper_instance.build_authorization_server_metadata(config) + expect(metadata.code_challenge_methods_supported).to eq(['S256']) + end + end + + describe 'route registration' do + let(:mock_app) do + Class.new do + attr_reader :routes + + def initialize + @routes = { get: {}, post: {}, options: {} } + end + + def get(path, &block) + @routes[:get][path] = block + end + + def post(path, &block) + @routes[:post][path] = block + end + + def options(path, &block) + @routes[:options][path] = block + end + + def helpers(_mod) + # No-op for route registration tests + end + end.new + end + + before do + AuthMcpServer::Routes::OAuthEndpoints.registered(mock_app) + end + + it 'registers GET /.well-known/oauth-authorization-server' do + expect(mock_app.routes[:get]).to have_key('/.well-known/oauth-authorization-server') + end + + it 'registers OPTIONS /.well-known/oauth-authorization-server' do + expect(mock_app.routes[:options]).to have_key('/.well-known/oauth-authorization-server') + end + end +end diff --git a/examples/auth/spec/routes/oauth_endpoints/authorize_spec.rb b/examples/auth/spec/routes/oauth_endpoints/authorize_spec.rb new file mode 100644 index 00000000..d8db545c --- /dev/null +++ b/examples/auth/spec/routes/oauth_endpoints/authorize_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/cors_middleware' +require_relative '../../../lib/routes/oauth_endpoints' + +RSpec.describe 'OAuth Authorize Endpoint' do + describe 'route registration' do + let(:mock_app) do + Class.new do + attr_reader :routes + + def initialize + @routes = { get: {}, options: {} } + end + + def get(path, &block) + @routes[:get][path] = block + end + + def options(path, &block) + @routes[:options][path] = block + end + + def post(path, &block) + @routes[:post] ||= {} + @routes[:post][path] = block + end + + def helpers(_mod) + # No-op for route registration tests + end + end.new + end + + before do + AuthMcpServer::Routes::OAuthEndpoints.registered(mock_app) + end + + it 'registers GET /oauth/authorize' do + expect(mock_app.routes[:get]).to have_key('/oauth/authorize') + end + + it 'registers OPTIONS /oauth/authorize' do + expect(mock_app.routes[:options]).to have_key('/oauth/authorize') + end + end + + describe 'authorize redirect behavior' do + let(:helper_instance) do + Class.new do + include AuthMcpServer::Routes::OAuthEndpoints::Helpers + end.new + end + + it 'derives authorization endpoint correctly' do + config = AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + oauth_authorize_url: 'https://auth.example.com/authorize' + ) + + result = helper_instance.derive_authorization_endpoint(config) + expect(result).to eq('https://auth.example.com/authorize') + end + + it 'uses auth_server_url pattern when oauth_authorize_url not set' do + config = AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + auth_server_url: 'https://keycloak.example.com/realms/mcp' + ) + + result = helper_instance.derive_authorization_endpoint(config) + expect(result).to eq('https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth') + end + + it 'falls back to server_url pattern' do + config = AuthMcpServer::Config.new(server_url: 'https://api.example.com') + + result = helper_instance.derive_authorization_endpoint(config) + expect(result).to eq('https://api.example.com/oauth/authorize') + end + end +end diff --git a/examples/auth/spec/routes/oauth_endpoints/openid_configuration_spec.rb b/examples/auth/spec/routes/oauth_endpoints/openid_configuration_spec.rb new file mode 100644 index 00000000..b9f70de1 --- /dev/null +++ b/examples/auth/spec/routes/oauth_endpoints/openid_configuration_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/cors_middleware' +require_relative '../../../lib/routes/oauth_endpoints' + +RSpec.describe 'OpenID Configuration Endpoint' do + let(:config) do + AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + auth_server_url: 'https://auth.example.com/realms/mcp', + allowed_scopes: 'mcp:read mcp:write' + ) + end + + let(:helper_instance) do + Class.new do + include AuthMcpServer::Routes::OAuthEndpoints::Helpers + end.new + end + + describe 'build_openid_configuration' do + it 'includes all authorization server metadata fields' do + oidc = helper_instance.build_openid_configuration(config) + expect(oidc.issuer).not_to be_empty + expect(oidc.authorization_endpoint).not_to be_empty + expect(oidc.token_endpoint).not_to be_empty + end + + it 'merges OIDC scopes with config scopes' do + oidc = helper_instance.build_openid_configuration(config) + scopes = oidc.scopes_supported + + # Should have OIDC scopes + expect(scopes).to include('openid') + expect(scopes).to include('profile') + expect(scopes).to include('email') + + # Should have config scopes + expect(scopes).to include('mcp:read') + expect(scopes).to include('mcp:write') + end + + it 'deduplicates scopes' do + config_with_oidc = AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + allowed_scopes: 'openid profile custom' + ) + + oidc = helper_instance.build_openid_configuration(config_with_oidc) + scopes = oidc.scopes_supported + + # Should not have duplicates + expect(scopes.count('openid')).to eq(1) + expect(scopes.count('profile')).to eq(1) + end + + it 'derives userinfo endpoint from auth_server_url' do + oidc = helper_instance.build_openid_configuration(config) + expect(oidc.userinfo_endpoint).to eq('https://auth.example.com/realms/mcp/protocol/openid-connect/userinfo') + end + + it 'sets userinfo to nil when no auth_server_url' do + config_no_auth = AuthMcpServer::Config.new(server_url: 'https://api.example.com') + oidc = helper_instance.build_openid_configuration(config_no_auth) + expect(oidc.userinfo_endpoint).to be_nil + end + + it 'includes subject types' do + oidc = helper_instance.build_openid_configuration(config) + expect(oidc.subject_types_supported).to eq(['public']) + end + + it 'includes ID token signing algorithms' do + oidc = helper_instance.build_openid_configuration(config) + expect(oidc.id_token_signing_alg_values_supported).to eq(%w[RS256 ES256]) + end + end + + describe 'derive_authorization_endpoint' do + it 'uses oauth_authorize_url when set' do + config_with_url = AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + oauth_authorize_url: 'https://custom.example.com/authorize' + ) + + result = helper_instance.derive_authorization_endpoint(config_with_url) + expect(result).to eq('https://custom.example.com/authorize') + end + + it 'derives from auth_server_url when oauth_authorize_url not set' do + result = helper_instance.derive_authorization_endpoint(config) + expect(result).to eq('https://auth.example.com/realms/mcp/protocol/openid-connect/auth') + end + + it 'falls back to server_url when neither is set' do + config_minimal = AuthMcpServer::Config.new(server_url: 'https://api.example.com') + result = helper_instance.derive_authorization_endpoint(config_minimal) + expect(result).to eq('https://api.example.com/oauth/authorize') + end + end + + describe 'route registration' do + let(:mock_app) do + Class.new do + attr_reader :routes + + def initialize + @routes = { get: {}, post: {}, options: {} } + end + + def get(path, &block) + @routes[:get][path] = block + end + + def post(path, &block) + @routes[:post][path] = block + end + + def options(path, &block) + @routes[:options][path] = block + end + + def helpers(_mod) + # No-op for route registration tests + end + end.new + end + + before do + AuthMcpServer::Routes::OAuthEndpoints.registered(mock_app) + end + + it 'registers GET /.well-known/openid-configuration' do + expect(mock_app.routes[:get]).to have_key('/.well-known/openid-configuration') + end + + it 'registers OPTIONS /.well-known/openid-configuration' do + expect(mock_app.routes[:options]).to have_key('/.well-known/openid-configuration') + end + end +end diff --git a/examples/auth/spec/routes/oauth_endpoints/protected_resource_spec.rb b/examples/auth/spec/routes/oauth_endpoints/protected_resource_spec.rb new file mode 100644 index 00000000..e9bbafd7 --- /dev/null +++ b/examples/auth/spec/routes/oauth_endpoints/protected_resource_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/cors_middleware' +require_relative '../../../lib/routes/oauth_endpoints' + +RSpec.describe 'Protected Resource Metadata Endpoints' do + let(:config) do + AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + allowed_scopes: 'openid profile email mcp:read mcp:write' + ) + end + + describe 'build_protected_resource_metadata' do + let(:helper_instance) do + Class.new do + include AuthMcpServer::Routes::OAuthEndpoints::Helpers + end.new + end + + it 'builds metadata with resource URL' do + metadata = helper_instance.build_protected_resource_metadata(config) + expect(metadata.resource).to eq('https://api.example.com/mcp') + end + + it 'includes authorization servers' do + metadata = helper_instance.build_protected_resource_metadata(config) + expect(metadata.authorization_servers).to eq(['https://api.example.com']) + end + + it 'includes scopes from config' do + metadata = helper_instance.build_protected_resource_metadata(config) + expect(metadata.scopes_supported).to eq(%w[openid profile email mcp:read mcp:write]) + end + + it 'includes bearer methods' do + metadata = helper_instance.build_protected_resource_metadata(config) + expect(metadata.bearer_methods_supported).to eq(%w[header query]) + end + + it 'includes documentation URL' do + metadata = helper_instance.build_protected_resource_metadata(config) + expect(metadata.resource_documentation).to eq('https://api.example.com/docs') + end + + it 'returns valid JSON' do + metadata = helper_instance.build_protected_resource_metadata(config) + json = metadata.to_json + parsed = JSON.parse(json) + + expect(parsed['resource']).to eq('https://api.example.com/mcp') + expect(parsed['authorization_servers']).to eq(['https://api.example.com']) + expect(parsed['scopes_supported']).to include('mcp:read') + expect(parsed['bearer_methods_supported']).to eq(%w[header query]) + end + end + + describe 'route registration' do + let(:mock_app) do + Class.new do + attr_reader :routes + + def initialize + @routes = { get: {}, post: {}, options: {} } + end + + def get(path, &block) + @routes[:get][path] = block + end + + def post(path, &block) + @routes[:post][path] = block + end + + def options(path, &block) + @routes[:options][path] = block + end + + def helpers(_mod) + # No-op for route registration tests + end + + def settings + @settings ||= OpenStruct.new(config: nil) + end + end.new + end + + before do + AuthMcpServer::Routes::OAuthEndpoints.registered(mock_app) + end + + it 'registers GET /.well-known/oauth-protected-resource' do + expect(mock_app.routes[:get]).to have_key('/.well-known/oauth-protected-resource') + end + + it 'registers GET /.well-known/oauth-protected-resource/mcp' do + expect(mock_app.routes[:get]).to have_key('/.well-known/oauth-protected-resource/mcp') + end + + it 'registers OPTIONS /.well-known/oauth-protected-resource' do + expect(mock_app.routes[:options]).to have_key('/.well-known/oauth-protected-resource') + end + + it 'registers OPTIONS /.well-known/oauth-protected-resource/mcp' do + expect(mock_app.routes[:options]).to have_key('/.well-known/oauth-protected-resource/mcp') + end + end +end diff --git a/examples/auth/spec/routes/oauth_endpoints/register_spec.rb b/examples/auth/spec/routes/oauth_endpoints/register_spec.rb new file mode 100644 index 00000000..dffd984e --- /dev/null +++ b/examples/auth/spec/routes/oauth_endpoints/register_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../../lib/config' +require_relative '../../../lib/middleware/cors_middleware' +require_relative '../../../lib/routes/oauth_endpoints' + +RSpec.describe 'OAuth Register Endpoint' do + let(:helper_instance) do + Class.new do + include AuthMcpServer::Routes::OAuthEndpoints::Helpers + end.new + end + + describe 'build_client_registration_response' do + let(:config) do + AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + client_id: 'test-client-id', + client_secret: 'test-client-secret' + ) + end + + it 'includes client_id from config' do + response = helper_instance.build_client_registration_response(config, {}) + expect(response.client_id).to eq('test-client-id') + end + + it 'includes client_secret when present' do + response = helper_instance.build_client_registration_response(config, {}) + expect(response.client_secret).to eq('test-client-secret') + end + + it 'sets token_endpoint_auth_method to client_secret_post when secret present' do + response = helper_instance.build_client_registration_response(config, {}) + expect(response.token_endpoint_auth_method).to eq('client_secret_post') + end + + it 'omits client_secret for public clients' do + public_config = AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + client_id: 'public-client' + ) + + response = helper_instance.build_client_registration_response(public_config, {}) + expect(response.client_secret).to be_nil + end + + it 'sets token_endpoint_auth_method to none for public clients' do + public_config = AuthMcpServer::Config.new( + server_url: 'https://api.example.com', + client_id: 'public-client' + ) + + response = helper_instance.build_client_registration_response(public_config, {}) + expect(response.token_endpoint_auth_method).to eq('none') + end + + it 'extracts redirect_uris from request body' do + body = { 'redirect_uris' => ['https://app.example.com/callback'] } + response = helper_instance.build_client_registration_response(config, body) + expect(response.redirect_uris).to eq(['https://app.example.com/callback']) + end + + it 'uses empty array when redirect_uris not provided' do + response = helper_instance.build_client_registration_response(config, {}) + expect(response.redirect_uris).to eq([]) + end + + it 'sets client_id_issued_at to current time' do + before_time = Time.now.to_i + response = helper_instance.build_client_registration_response(config, {}) + after_time = Time.now.to_i + + expect(response.client_id_issued_at).to be >= before_time + expect(response.client_id_issued_at).to be <= after_time + end + + it 'sets client_secret_expires_at to 0 (never expires)' do + response = helper_instance.build_client_registration_response(config, {}) + expect(response.client_secret_expires_at).to eq(0) + end + + it 'includes standard grant types' do + response = helper_instance.build_client_registration_response(config, {}) + expect(response.grant_types).to eq(%w[authorization_code refresh_token]) + end + + it 'includes standard response types' do + response = helper_instance.build_client_registration_response(config, {}) + expect(response.response_types).to eq(['code']) + end + + it 'generates valid JSON' do + body = { 'redirect_uris' => ['https://app.example.com/callback'] } + response = helper_instance.build_client_registration_response(config, body) + + json = response.to_json + parsed = JSON.parse(json) + + expect(parsed['client_id']).to eq('test-client-id') + expect(parsed['client_secret']).to eq('test-client-secret') + expect(parsed['redirect_uris']).to eq(['https://app.example.com/callback']) + end + end + + describe 'route registration' do + let(:mock_app) do + Class.new do + attr_reader :routes + + def initialize + @routes = { get: {}, post: {}, options: {} } + end + + def get(path, &block) + @routes[:get][path] = block + end + + def post(path, &block) + @routes[:post][path] = block + end + + def options(path, &block) + @routes[:options][path] = block + end + + def helpers(_mod) + # No-op for route registration tests + end + end.new + end + + before do + AuthMcpServer::Routes::OAuthEndpoints.registered(mock_app) + end + + it 'registers POST /oauth/register' do + expect(mock_app.routes[:post]).to have_key('/oauth/register') + end + + it 'registers OPTIONS /oauth/register' do + expect(mock_app.routes[:options]).to have_key('/oauth/register') + end + end +end diff --git a/examples/auth/spec/tools/weather_tools_spec.rb b/examples/auth/spec/tools/weather_tools_spec.rb new file mode 100644 index 00000000..7475cdda --- /dev/null +++ b/examples/auth/spec/tools/weather_tools_spec.rb @@ -0,0 +1,335 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../../lib/routes/mcp_handler' +require_relative '../../lib/tools/weather_tools' + +RSpec.describe AuthMcpServer::WeatherTools do + let(:mcp_handler) { AuthMcpServer::McpHandler.new({ name: 'test', version: '1.0.0' }) } + + # Mock config for tests + let(:auth_disabled_config) do + Class.new do + def auth_disabled + true + end + end.new + end + + let(:auth_enabled_config) do + Class.new do + def auth_disabled + false + end + end.new + end + + # Mock auth context + let(:auth_context_with_read_scope) do + Class.new do + def has_scope?(scope) + %w[mcp:read openid].include?(scope.downcase) + end + + def scopes + 'mcp:read openid' + end + end.new + end + + let(:auth_context_with_admin_scope) do + Class.new do + def has_scope?(scope) + %w[mcp:admin mcp:read openid].include?(scope.downcase) + end + + def scopes + 'mcp:admin mcp:read openid' + end + end.new + end + + let(:auth_context_no_scopes) do + Class.new do + def has_scope?(_scope) + false + end + + def scopes + '' + end + end.new + end + + describe '.register' do + before { described_class.register(mcp_handler, auth_disabled_config) } + + it 'registers get-weather tool' do + tools = mcp_handler.get_tools + tool_names = tools.map { |t| t[:name] } + expect(tool_names).to include('get-weather') + end + + it 'registers get-forecast tool' do + tools = mcp_handler.get_tools + tool_names = tools.map { |t| t[:name] } + expect(tool_names).to include('get-forecast') + end + + it 'registers get-weather-alerts tool' do + tools = mcp_handler.get_tools + tool_names = tools.map { |t| t[:name] } + expect(tool_names).to include('get-weather-alerts') + end + + it 'registers all three tools' do + expect(mcp_handler.get_tools.length).to eq(3) + end + end + + describe 'get-weather tool' do + before { described_class.register(mcp_handler, auth_disabled_config) } + + it 'returns weather data for city' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather', 'arguments' => { 'city' => 'London' } }, + auth_context_no_scopes + ) + + content = JSON.parse(result[:content][0][:text]) + expect(content['city']).to eq('London') + expect(content['temperature']).to be_a(Integer) + expect(content['condition']).to be_a(String) + expect(content['humidity']).to be_a(Integer) + expect(content['windSpeed']).to be_a(Integer) + end + + it 'returns deterministic weather for same city' do + result1 = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather', 'arguments' => { 'city' => 'Paris' } }, + auth_context_no_scopes + ) + result2 = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather', 'arguments' => { 'city' => 'Paris' } }, + auth_context_no_scopes + ) + + content1 = JSON.parse(result1[:content][0][:text]) + content2 = JSON.parse(result2[:content][0][:text]) + expect(content1).to eq(content2) + end + + it 'handles missing city' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather', 'arguments' => {} }, + auth_context_no_scopes + ) + + content = JSON.parse(result[:content][0][:text]) + expect(content['city']).to eq('Unknown') + end + end + + describe 'get-forecast tool' do + context 'with auth disabled' do + before { described_class.register(mcp_handler, auth_disabled_config) } + + it 'returns forecast without scope check' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-forecast', 'arguments' => { 'city' => 'Tokyo' } }, + auth_context_no_scopes + ) + + content = JSON.parse(result[:content][0][:text]) + expect(content['city']).to eq('Tokyo') + expect(content['forecast']).to be_an(Array) + expect(content['forecast'].length).to eq(5) + end + + it 'includes day names in forecast' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-forecast', 'arguments' => { 'city' => 'Berlin' } }, + auth_context_no_scopes + ) + + content = JSON.parse(result[:content][0][:text]) + days = content['forecast'].map { |f| f['day'] } + expect(days).to include('Today', 'Tomorrow') + end + + it 'respects custom days parameter' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-forecast', 'arguments' => { 'city' => 'Rome', 'days' => 3 } }, + auth_context_no_scopes + ) + + content = JSON.parse(result[:content][0][:text]) + expect(content['forecast'].length).to eq(3) + end + end + + context 'with auth enabled' do + before { described_class.register(mcp_handler, auth_enabled_config) } + + it 'allows access with mcp:read scope' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-forecast', 'arguments' => { 'city' => 'Sydney' } }, + auth_context_with_read_scope + ) + + expect(result[:isError]).to be_nil + content = JSON.parse(result[:content][0][:text]) + expect(content['city']).to eq('Sydney') + end + + it 'denies access without mcp:read scope' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-forecast', 'arguments' => { 'city' => 'Sydney' } }, + auth_context_no_scopes + ) + + expect(result[:isError]).to be(true) + content = JSON.parse(result[:content][0][:text]) + expect(content['error']).to eq('access_denied') + expect(content['message']).to include('mcp:read') + end + end + end + + describe 'get-weather-alerts tool' do + context 'with auth disabled' do + before { described_class.register(mcp_handler, auth_disabled_config) } + + it 'returns alerts without scope check' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather-alerts', 'arguments' => { 'region' => 'California' } }, + auth_context_no_scopes + ) + + content = JSON.parse(result[:content][0][:text]) + expect(content['region']).to eq('California') + expect(content['alerts']).to be_an(Array) + end + end + + context 'with auth enabled' do + before { described_class.register(mcp_handler, auth_enabled_config) } + + it 'allows access with mcp:admin scope' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather-alerts', 'arguments' => { 'region' => 'Texas' } }, + auth_context_with_admin_scope + ) + + expect(result[:isError]).to be_nil + content = JSON.parse(result[:content][0][:text]) + expect(content['region']).to eq('Texas') + end + + it 'denies access without mcp:admin scope' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather-alerts', 'arguments' => { 'region' => 'Texas' } }, + auth_context_with_read_scope + ) + + expect(result[:isError]).to be(true) + content = JSON.parse(result[:content][0][:text]) + expect(content['error']).to eq('access_denied') + expect(content['message']).to include('mcp:admin') + end + + it 'denies access with no scopes' do + result = mcp_handler.dispatch_method( + 'tools/call', + { 'name' => 'get-weather-alerts', 'arguments' => { 'region' => 'Texas' } }, + auth_context_no_scopes + ) + + expect(result[:isError]).to be(true) + end + end + end + + describe '.get_simulated_weather' do + it 'returns weather hash with all fields' do + weather = described_class.get_simulated_weather('London') + + expect(weather[:city]).to eq('London') + expect(weather[:temperature]).to be_between(10, 35) + expect(weather[:humidity]).to be_between(40, 79) + expect(weather[:windSpeed]).to be_between(5, 29) + expect(AuthMcpServer::WeatherTools::CONDITIONS).to include(weather[:condition]) + end + end + + describe '.get_simulated_forecast' do + it 'returns array of forecasts' do + forecast = described_class.get_simulated_forecast('Paris') + + expect(forecast).to be_an(Array) + expect(forecast.length).to eq(5) + end + + it 'includes high and low temperatures' do + forecast = described_class.get_simulated_forecast('Paris') + + forecast.each do |day| + expect(day[:high]).to be > day[:low] + end + end + + it 'respects days parameter' do + forecast = described_class.get_simulated_forecast('Paris', 3) + expect(forecast.length).to eq(3) + end + end + + describe '.get_simulated_alerts' do + it 'returns array of alerts' do + alerts = described_class.get_simulated_alerts('California') + expect(alerts).to be_an(Array) + end + + it 'includes alert fields when present' do + # Find a region that has alerts + %w[California Texas Florida].each do |region| + alerts = described_class.get_simulated_alerts(region) + next if alerts.empty? + + expect(alerts.first).to have_key(:type) + expect(alerts.first).to have_key(:severity) + expect(alerts.first).to have_key(:message) + break + end + end + end + + describe '.error_result' do + it 'returns error format with isError true' do + result = described_class.error_result('Test error') + + expect(result[:isError]).to be(true) + expect(result[:content]).to be_an(Array) + end + + it 'includes error message in content' do + result = described_class.error_result('Access denied') + + content = JSON.parse(result[:content][0][:text]) + expect(content['error']).to eq('access_denied') + expect(content['message']).to eq('Access denied') + end + end +end diff --git a/examples/client_example_json.rb b/examples/client_example_json.rb index 9e345f6a..5f9fd787 100755 --- a/examples/client_example_json.rb +++ b/examples/client_example_json.rb @@ -42,10 +42,10 @@ begin # Create agent with JSON server configuration config = GopherOrch::ConfigBuilder.create - .with_provider(provider) - .with_model(model) - .with_server_config(SERVER_CONFIG) - .build + .with_provider(provider) + .with_model(model) + .with_server_config(SERVER_CONFIG) + .build agent = GopherOrch::Agent.create(config) puts 'GopherAgent created!' diff --git a/gopher_orch.gemspec b/gopher_orch.gemspec index 31cd3594..07f93613 100644 --- a/gopher_orch.gemspec +++ b/gopher_orch.gemspec @@ -15,6 +15,7 @@ Gem::Specification.new do |spec| spec.metadata['homepage_uri'] = spec.homepage spec.metadata['source_code_uri'] = spec.homepage spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md" + spec.metadata['rubygems_mfa_required'] = 'true' spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |f| diff --git a/lib/gopher_orch/agent.rb b/lib/gopher_orch/agent.rb index eebfc789..1a5d691d 100644 --- a/lib/gopher_orch/agent.rb +++ b/lib/gopher_orch/agent.rb @@ -50,10 +50,10 @@ def self.create(config) # @raise [AgentError] if agent creation fails def self.create_with_api_key(provider, model, api_key) config = ConfigBuilder.create - .with_provider(provider) - .with_model(model) - .with_api_key(api_key) - .build + .with_provider(provider) + .with_model(model) + .with_api_key(api_key) + .build create(config) end @@ -67,10 +67,10 @@ def self.create_with_api_key(provider, model, api_key) # @raise [AgentError] if agent creation fails def self.create_with_server_config(provider, model, server_config) config = ConfigBuilder.create - .with_provider(provider) - .with_model(model) - .with_server_config(server_config) - .build + .with_provider(provider) + .with_model(model) + .with_server_config(server_config) + .build create(config) end diff --git a/lib/gopher_orch/auth/auth_context.rb b/lib/gopher_orch/auth/auth_context.rb new file mode 100644 index 00000000..ed60b392 --- /dev/null +++ b/lib/gopher_orch/auth/auth_context.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module GopherOrch + module Auth + # AuthContext represents the authentication context for a request. + # It contains information about the authenticated user, their scopes, + # and token metadata. + class AuthContext + attr_reader :user_id, :scopes, :audience, :token_expiry, :authenticated + + # Creates a new AuthContext instance. + # + # @param user_id [String] the user identifier + # @param scopes [String] space-separated list of scopes + # @param audience [String] the token audience + # @param token_expiry [Integer] Unix timestamp when the token expires + # @param authenticated [Boolean] whether the context is authenticated + def initialize(user_id:, scopes:, audience:, token_expiry:, authenticated:) + @user_id = user_id + @scopes = scopes + @audience = audience + @token_expiry = token_expiry + @authenticated = authenticated + end + + # Checks if the context has a required scope. + # Performs case-insensitive matching against space-separated scopes. + # + # @param required_scope [String, nil] the scope to check for + # @return [Boolean] true if the scope is present or not required + def has_scope?(required_scope) + return true if required_scope.nil? || required_scope.empty? + return false if @scopes.nil? || @scopes.empty? + + scope_list = @scopes.split + scope_list.any? { |s| s.casecmp(required_scope).zero? } + end + + # Creates an empty, unauthenticated context. + # + # @return [AuthContext] an empty context + def self.empty + new( + user_id: '', + scopes: '', + audience: '', + token_expiry: 0, + authenticated: false + ) + end + + # Creates an anonymous context for auth-disabled mode. + # The anonymous user is considered authenticated with the given scopes. + # + # @param scopes [String] space-separated list of scopes + # @return [AuthContext] an anonymous context with full access + def self.anonymous(scopes) + new( + user_id: 'anonymous', + scopes: scopes, + audience: '', + token_expiry: Time.now.to_i + 3600, + authenticated: true + ) + end + end + end +end diff --git a/lib/gopher_orch/auth/errors.rb b/lib/gopher_orch/auth/errors.rb new file mode 100644 index 00000000..4010164f --- /dev/null +++ b/lib/gopher_orch/auth/errors.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module GopherOrch + module Auth + # Error codes returned by the auth library. + # These codes match the native gopher-auth library error codes. + module ErrorCodes + SUCCESS = 0 + INVALID_TOKEN = -1000 + EXPIRED_TOKEN = -1001 + INVALID_SIGNATURE = -1002 + INVALID_ISSUER = -1003 + INVALID_AUDIENCE = -1004 + INSUFFICIENT_SCOPE = -1005 + JWKS_FETCH_FAILED = -1006 + INVALID_KEY = -1007 + NETWORK_ERROR = -1008 + INVALID_CONFIG = -1009 + NOT_INITIALIZED = -1012 + INTERNAL_ERROR = -1013 + + # Returns a human-readable description for an error code. + # + # @param code [Integer] the error code + # @return [String] the error description + def self.description(code) + DESCRIPTIONS[code] || "Unknown error: #{code}" + end + + DESCRIPTIONS = { + SUCCESS => 'Success', + INVALID_TOKEN => 'Invalid token format or structure', + EXPIRED_TOKEN => 'Token has expired', + INVALID_SIGNATURE => 'Token signature verification failed', + INVALID_ISSUER => 'Token issuer does not match', + INVALID_AUDIENCE => 'Token audience does not match', + INSUFFICIENT_SCOPE => 'Token does not have required scopes', + JWKS_FETCH_FAILED => 'Failed to fetch JWKS', + INVALID_KEY => 'Invalid key', + NETWORK_ERROR => 'Network error', + INVALID_CONFIG => 'Invalid configuration', + NOT_INITIALIZED => 'Auth library not initialized', + INTERNAL_ERROR => 'Internal error', + }.freeze + end + + # AuthError is raised when an authentication operation fails. + # It contains an error code and a descriptive message. + class AuthError < StandardError + attr_reader :code + + # Creates a new AuthError. + # + # @param code [Integer] the error code from ErrorCodes + # @param message [String, nil] optional custom message (uses default description if nil) + def initialize(code, message = nil) + @code = code + super(message || ErrorCodes.description(code)) + end + end + end +end diff --git a/lib/gopher_orch/auth/oauth/authorization_server_metadata.rb b/lib/gopher_orch/auth/oauth/authorization_server_metadata.rb new file mode 100644 index 00000000..0cbae8f6 --- /dev/null +++ b/lib/gopher_orch/auth/oauth/authorization_server_metadata.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'json' + +module GopherOrch + module Auth + module OAuth + # RFC 8414 - OAuth 2.0 Authorization Server Metadata + # Describes the configuration of an OAuth 2.0 authorization server. + class AuthorizationServerMetadata + attr_accessor :issuer, :authorization_endpoint, :token_endpoint, + :jwks_uri, :registration_endpoint, :scopes_supported, + :response_types_supported, :grant_types_supported, + :token_endpoint_auth_methods_supported, + :code_challenge_methods_supported + + # Creates a new AuthorizationServerMetadata instance. + # + # @param issuer [String] the authorization server's issuer identifier + # @param authorization_endpoint [String] URL of the authorization endpoint + # @param token_endpoint [String] URL of the token endpoint + # @param jwks_uri [String, nil] URL of the JSON Web Key Set + # @param registration_endpoint [String, nil] URL of the registration endpoint + # @param scopes_supported [Array] list of supported scopes + # @param response_types_supported [Array] supported response types + # @param grant_types_supported [Array] supported grant types + # @param token_endpoint_auth_methods_supported [Array] supported auth methods + # @param code_challenge_methods_supported [Array] supported PKCE methods + def initialize(**attrs) + @issuer = attrs[:issuer] || '' + @authorization_endpoint = attrs[:authorization_endpoint] || '' + @token_endpoint = attrs[:token_endpoint] || '' + @jwks_uri = attrs[:jwks_uri] + @registration_endpoint = attrs[:registration_endpoint] + @scopes_supported = attrs[:scopes_supported] || [] + @response_types_supported = attrs[:response_types_supported] || [] + @grant_types_supported = attrs[:grant_types_supported] || [] + @token_endpoint_auth_methods_supported = attrs[:token_endpoint_auth_methods_supported] || [] + @code_challenge_methods_supported = attrs[:code_challenge_methods_supported] || [] + end + + # Converts the metadata to a hash for JSON serialization. + # Omits nil optional fields. + # + # @return [Hash] the metadata as a hash + def to_h + result = { + issuer: @issuer, + authorization_endpoint: @authorization_endpoint, + token_endpoint: @token_endpoint, + scopes_supported: @scopes_supported, + response_types_supported: @response_types_supported, + grant_types_supported: @grant_types_supported, + token_endpoint_auth_methods_supported: @token_endpoint_auth_methods_supported, + code_challenge_methods_supported: @code_challenge_methods_supported, + } + result[:jwks_uri] = @jwks_uri if @jwks_uri + result[:registration_endpoint] = @registration_endpoint if @registration_endpoint + result + end + + # Converts the metadata to JSON. + # + # @param args [Array] arguments passed to JSON.generate + # @return [String] JSON representation + def to_json(*args) + to_h.to_json(*args) + end + end + end + end +end diff --git a/lib/gopher_orch/auth/oauth/client_registration_response.rb b/lib/gopher_orch/auth/oauth/client_registration_response.rb new file mode 100644 index 00000000..7864906b --- /dev/null +++ b/lib/gopher_orch/auth/oauth/client_registration_response.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'json' + +module GopherOrch + module Auth + module OAuth + # RFC 7591 - OAuth 2.0 Dynamic Client Registration Response + # Contains client credentials and metadata returned after registration. + class ClientRegistrationResponse + attr_accessor :client_id, :client_secret, :client_id_issued_at, + :client_secret_expires_at, :redirect_uris, :grant_types, + :response_types, :token_endpoint_auth_method + + # Creates a new ClientRegistrationResponse instance. + # + # @param client_id [String] the client identifier + # @param client_secret [String, nil] the client secret (optional for public clients) + # @param client_id_issued_at [Integer] Unix timestamp when client_id was issued + # @param client_secret_expires_at [Integer] Unix timestamp when secret expires (0 = never) + # @param redirect_uris [Array] registered redirect URIs + # @param grant_types [Array] allowed grant types + # @param response_types [Array] allowed response types + # @param token_endpoint_auth_method [String] authentication method for token endpoint + def initialize(**attrs) + @client_id = attrs[:client_id] || '' + @client_secret = attrs[:client_secret] + @client_id_issued_at = attrs[:client_id_issued_at] || 0 + @client_secret_expires_at = attrs[:client_secret_expires_at] || 0 + @redirect_uris = attrs[:redirect_uris] || [] + @grant_types = attrs[:grant_types] || [] + @response_types = attrs[:response_types] || [] + @token_endpoint_auth_method = attrs[:token_endpoint_auth_method] || 'none' + end + + # Converts the response to a hash for JSON serialization. + # Includes client_secret only if present. + # + # @return [Hash] the response as a hash + def to_h + result = { + client_id: @client_id, + client_id_issued_at: @client_id_issued_at, + client_secret_expires_at: @client_secret_expires_at, + redirect_uris: @redirect_uris, + grant_types: @grant_types, + response_types: @response_types, + token_endpoint_auth_method: @token_endpoint_auth_method, + } + result[:client_secret] = @client_secret if @client_secret + result + end + + # Converts the response to JSON. + # + # @param args [Array] arguments passed to JSON.generate + # @return [String] JSON representation + def to_json(*args) + to_h.to_json(*args) + end + end + end + end +end diff --git a/lib/gopher_orch/auth/oauth/openid_configuration.rb b/lib/gopher_orch/auth/oauth/openid_configuration.rb new file mode 100644 index 00000000..982028f7 --- /dev/null +++ b/lib/gopher_orch/auth/oauth/openid_configuration.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'json' +require_relative 'authorization_server_metadata' + +module GopherOrch + module Auth + module OAuth + # OpenID Connect Discovery Configuration + # Extends AuthorizationServerMetadata with OIDC-specific fields. + class OpenIdConfiguration < AuthorizationServerMetadata + attr_accessor :userinfo_endpoint, :subject_types_supported, + :id_token_signing_alg_values_supported + + # Creates a new OpenIdConfiguration instance. + # + # @param userinfo_endpoint [String, nil] URL of the userinfo endpoint + # @param subject_types_supported [Array] supported subject types + # @param id_token_signing_alg_values_supported [Array] supported signing algorithms + # @param attrs [Hash] additional attributes passed to parent class + def initialize(**attrs) + super + @userinfo_endpoint = attrs[:userinfo_endpoint] + @subject_types_supported = attrs[:subject_types_supported] || [] + @id_token_signing_alg_values_supported = attrs[:id_token_signing_alg_values_supported] || [] + end + + # Converts the configuration to a hash for JSON serialization. + # Includes parent fields and OIDC-specific fields. + # + # @return [Hash] the configuration as a hash + def to_h + result = super + result[:userinfo_endpoint] = @userinfo_endpoint if @userinfo_endpoint + result[:subject_types_supported] = @subject_types_supported + result[:id_token_signing_alg_values_supported] = @id_token_signing_alg_values_supported + result + end + end + end + end +end diff --git a/lib/gopher_orch/auth/oauth/protected_resource_metadata.rb b/lib/gopher_orch/auth/oauth/protected_resource_metadata.rb new file mode 100644 index 00000000..4e0f84c0 --- /dev/null +++ b/lib/gopher_orch/auth/oauth/protected_resource_metadata.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'json' + +module GopherOrch + module Auth + module OAuth + # RFC 9728 - OAuth 2.0 Protected Resource Metadata + # Describes the capabilities and requirements of a protected resource. + class ProtectedResourceMetadata + attr_accessor :resource, :authorization_servers, :scopes_supported, + :bearer_methods_supported, :resource_documentation + + # Creates a new ProtectedResourceMetadata instance. + # + # @param resource [String] the resource identifier (URL) + # @param authorization_servers [Array] list of authorization server URLs + # @param scopes_supported [Array] list of supported scopes + # @param bearer_methods_supported [Array] supported bearer token methods + # @param resource_documentation [String, nil] optional documentation URL + def initialize(**attrs) + @resource = attrs[:resource] || '' + @authorization_servers = attrs[:authorization_servers] || [] + @scopes_supported = attrs[:scopes_supported] || [] + @bearer_methods_supported = attrs[:bearer_methods_supported] || [] + @resource_documentation = attrs[:resource_documentation] + end + + # Converts the metadata to a hash for JSON serialization. + # Omits nil optional fields. + # + # @return [Hash] the metadata as a hash + def to_h + result = { + resource: @resource, + authorization_servers: @authorization_servers, + scopes_supported: @scopes_supported, + bearer_methods_supported: @bearer_methods_supported, + } + result[:resource_documentation] = @resource_documentation if @resource_documentation + result + end + + # Converts the metadata to JSON. + # + # @param args [Array] arguments passed to JSON.generate + # @return [String] JSON representation + def to_json(*args) + to_h.to_json(*args) + end + end + end + end +end diff --git a/lib/gopher_orch/auth/www_authenticate.rb b/lib/gopher_orch/auth/www_authenticate.rb new file mode 100644 index 00000000..90ebef00 --- /dev/null +++ b/lib/gopher_orch/auth/www_authenticate.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module GopherOrch + module Auth + # WWW-Authenticate header generation utilities for RFC 6750 Bearer challenges. + module WwwAuthenticate + # Generates a WWW-Authenticate header value for Bearer authentication. + # Follows RFC 6750 format with OAuth 2.0 Protected Resource Metadata extension. + # + # @param realm [String] the authentication realm + # @param resource_metadata_url [String] URL to the protected resource metadata + # @param scopes [String] space-separated list of required scopes + # @param error [String] error code (e.g., 'invalid_token', 'invalid_request') + # @param description [String] human-readable error description + # @return [String] the WWW-Authenticate header value + def self.generate(realm:, resource_metadata_url:, scopes:, error:, description:) + parts = [ + "Bearer realm=\"#{escape(realm)}\"", + "resource_metadata=\"#{escape(resource_metadata_url)}\"", + "scope=\"#{escape(scopes)}\"", + "error=\"#{escape(error)}\"", + "error_description=\"#{escape(description)}\"", + ] + parts.join(', ') + end + + # Escapes a string value for inclusion in a quoted HTTP header parameter. + # Escapes backslashes and double quotes per RFC 7230. + # + # @param value [String, nil] the value to escape + # @return [String] the escaped value + def self.escape(value) + return '' if value.nil? || value.empty? + + value.to_s.gsub('\\', '\\\\\\\\').gsub('"', '\\"') + end + end + end +end diff --git a/lib/gopher_orch/native.rb b/lib/gopher_orch/native.rb index e8809134..0cbe79c9 100644 --- a/lib/gopher_orch/native.rb +++ b/lib/gopher_orch/native.rb @@ -155,11 +155,11 @@ def find_library File.join(File.dirname(__FILE__), '..', '..', 'native', 'lib', "#{lib_name}.#{extension}"), # System paths "/usr/local/lib/#{lib_name}.#{extension}", - "/opt/homebrew/lib/#{lib_name}.#{extension}" + "/opt/homebrew/lib/#{lib_name}.#{extension}", ] # Check environment variable - env_path = ENV['GOPHER_ORCH_LIBRARY_PATH'] + env_path = ENV.fetch('GOPHER_ORCH_LIBRARY_PATH', nil) candidates.unshift(env_path) if env_path candidates.find { |path| File.exist?(path) } diff --git a/spec/agent_result_spec.rb b/spec/agent_result_spec.rb index 86b5ed75..89290ddc 100644 --- a/spec/agent_result_spec.rb +++ b/spec/agent_result_spec.rb @@ -3,7 +3,7 @@ RSpec.describe GopherOrch::AgentResult do describe '.success' do it 'creates a successful result' do - result = GopherOrch::AgentResult.success('Hello, world!') + result = described_class.success('Hello, world!') expect(result.response).to eq('Hello, world!') expect(result.status.success?).to be true @@ -16,7 +16,7 @@ describe '.error' do it 'creates an error result' do - result = GopherOrch::AgentResult.error('Something went wrong') + result = described_class.error('Something went wrong') expect(result.response).to eq('') expect(result.status.success?).to be false @@ -28,7 +28,7 @@ describe '.timeout' do it 'creates a timeout result' do - result = GopherOrch::AgentResult.timeout('Operation timed out') + result = described_class.timeout('Operation timed out') expect(result.response).to eq('') expect(result.status.success?).to be false diff --git a/spec/agent_result_status_spec.rb b/spec/agent_result_status_spec.rb index 2c8932f4..34cbfd20 100644 --- a/spec/agent_result_status_spec.rb +++ b/spec/agent_result_status_spec.rb @@ -3,7 +3,7 @@ RSpec.describe GopherOrch::AgentResultStatus do describe '.success' do it 'creates a success status' do - status = GopherOrch::AgentResultStatus.success + status = described_class.success expect(status.value).to eq(GopherOrch::AgentResultStatus::SUCCESS) expect(status.success?).to be true end @@ -11,7 +11,7 @@ describe '.error' do it 'creates an error status' do - status = GopherOrch::AgentResultStatus.error + status = described_class.error expect(status.value).to eq(GopherOrch::AgentResultStatus::ERROR) expect(status.success?).to be false end @@ -19,7 +19,7 @@ describe '.timeout' do it 'creates a timeout status' do - status = GopherOrch::AgentResultStatus.timeout + status = described_class.timeout expect(status.value).to eq(GopherOrch::AgentResultStatus::TIMEOUT) expect(status.success?).to be false end @@ -27,7 +27,7 @@ describe '.max_iterations_reached' do it 'creates a max iterations reached status' do - status = GopherOrch::AgentResultStatus.max_iterations_reached + status = described_class.max_iterations_reached expect(status.value).to eq(GopherOrch::AgentResultStatus::MAX_ITERATIONS_REACHED) expect(status.success?).to be false end @@ -35,23 +35,23 @@ describe '#to_s' do it 'returns the status value' do - status = GopherOrch::AgentResultStatus.success + status = described_class.success expect(status.to_s).to eq('SUCCESS') end end describe '#==' do it 'compares with another AgentResultStatus' do - status1 = GopherOrch::AgentResultStatus.success - status2 = GopherOrch::AgentResultStatus.success - status3 = GopherOrch::AgentResultStatus.error + status1 = described_class.success + status2 = described_class.success + status3 = described_class.error expect(status1).to eq(status2) expect(status1).not_to eq(status3) end it 'compares with a String' do - status = GopherOrch::AgentResultStatus.success + status = described_class.success expect(status).to eq('SUCCESS') expect(status).not_to eq('ERROR') diff --git a/spec/config_builder_spec.rb b/spec/config_builder_spec.rb index be093743..f90022f5 100644 --- a/spec/config_builder_spec.rb +++ b/spec/config_builder_spec.rb @@ -3,18 +3,18 @@ RSpec.describe GopherOrch::ConfigBuilder do describe '.create' do it 'returns a new ConfigBuilder' do - expect(GopherOrch::ConfigBuilder.create).to be_a(GopherOrch::ConfigBuilder) + expect(described_class.create).to be_a(described_class) end end describe '#build' do context 'with API key' do it 'creates a config with API key' do - config = GopherOrch::ConfigBuilder.create - .with_provider('TestProvider') - .with_model('test-model') - .with_api_key('test-key') - .build + config = described_class.create + .with_provider('TestProvider') + .with_model('test-model') + .with_api_key('test-key') + .build expect(config.provider).to eq('TestProvider') expect(config.model).to eq('test-model') @@ -28,11 +28,11 @@ it 'creates a config with server config' do server_config = '{"servers": []}' - config = GopherOrch::ConfigBuilder.create - .with_provider('TestProvider') - .with_model('test-model') - .with_server_config(server_config) - .build + config = described_class.create + .with_provider('TestProvider') + .with_model('test-model') + .with_server_config(server_config) + .build expect(config.provider).to eq('TestProvider') expect(config.model).to eq('test-model') @@ -44,7 +44,7 @@ context 'with empty values' do it 'creates a config with default values' do - config = GopherOrch::ConfigBuilder.create.build + config = described_class.create.build expect(config.provider).to eq('') expect(config.model).to eq('') @@ -58,7 +58,7 @@ describe 'fluent interface' do it 'returns self from all builder methods' do - builder = GopherOrch::ConfigBuilder.create + builder = described_class.create expect(builder.with_provider('Test')).to eq(builder) expect(builder.with_model('test')).to eq(builder) diff --git a/spec/gopher_orch/auth/auth_context_spec.rb b/spec/gopher_orch/auth/auth_context_spec.rb new file mode 100644 index 00000000..a210a660 --- /dev/null +++ b/spec/gopher_orch/auth/auth_context_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gopher_orch/auth/auth_context' + +RSpec.describe GopherOrch::Auth::AuthContext do + describe '#initialize' do + it 'creates an AuthContext with all attributes' do + context = described_class.new( + user_id: 'user-123', + scopes: 'read write admin', + audience: 'https://api.example.com', + token_expiry: 1_700_000_000, + authenticated: true + ) + + expect(context.user_id).to eq('user-123') + expect(context.scopes).to eq('read write admin') + expect(context.audience).to eq('https://api.example.com') + expect(context.token_expiry).to eq(1_700_000_000) + expect(context.authenticated).to be true + end + end + + describe '#has_scope?' do + let(:context) do + described_class.new( + user_id: 'user-123', + scopes: 'mcp:read mcp:write mcp:admin', + audience: '', + token_expiry: 0, + authenticated: true + ) + end + + it 'returns true when scope is present' do + expect(context.has_scope?('mcp:read')).to be true + expect(context.has_scope?('mcp:write')).to be true + expect(context.has_scope?('mcp:admin')).to be true + end + + it 'returns false when scope is not present' do + expect(context.has_scope?('mcp:delete')).to be false + expect(context.has_scope?('other:scope')).to be false + end + + it 'performs case-insensitive matching' do + expect(context.has_scope?('MCP:READ')).to be true + expect(context.has_scope?('Mcp:Write')).to be true + expect(context.has_scope?('MCP:ADMIN')).to be true + end + + it 'returns true for nil required scope' do + expect(context.has_scope?(nil)).to be true + end + + it 'returns true for empty required scope' do + expect(context.has_scope?('')).to be true + end + + context 'with empty scopes' do + let(:empty_scopes_context) do + described_class.new( + user_id: 'user-123', + scopes: '', + audience: '', + token_expiry: 0, + authenticated: true + ) + end + + it 'returns false for any required scope' do + expect(empty_scopes_context.has_scope?('mcp:read')).to be false + end + + it 'returns true for nil required scope' do + expect(empty_scopes_context.has_scope?(nil)).to be true + end + end + + context 'with nil scopes' do + let(:nil_scopes_context) do + described_class.new( + user_id: 'user-123', + scopes: nil, + audience: '', + token_expiry: 0, + authenticated: true + ) + end + + it 'returns false for any required scope' do + expect(nil_scopes_context.has_scope?('mcp:read')).to be false + end + end + end + + describe '.empty' do + it 'creates an unauthenticated empty context' do + context = described_class.empty + + expect(context.user_id).to eq('') + expect(context.scopes).to eq('') + expect(context.audience).to eq('') + expect(context.token_expiry).to eq(0) + expect(context.authenticated).to be false + end + end + + describe '.anonymous' do + it 'creates an authenticated anonymous context with given scopes' do + context = described_class.anonymous('read write admin') + + expect(context.user_id).to eq('anonymous') + expect(context.scopes).to eq('read write admin') + expect(context.audience).to eq('') + expect(context.authenticated).to be true + end + + it 'sets token_expiry to one hour from now' do + before_time = Time.now.to_i + context = described_class.anonymous('read') + after_time = Time.now.to_i + + expect(context.token_expiry).to be >= before_time + 3600 + expect(context.token_expiry).to be <= after_time + 3600 + end + + it 'allows scope checking on anonymous context' do + context = described_class.anonymous('mcp:read mcp:write') + + expect(context.has_scope?('mcp:read')).to be true + expect(context.has_scope?('mcp:write')).to be true + expect(context.has_scope?('mcp:admin')).to be false + end + end +end diff --git a/spec/gopher_orch/auth/errors_spec.rb b/spec/gopher_orch/auth/errors_spec.rb new file mode 100644 index 00000000..0af3e586 --- /dev/null +++ b/spec/gopher_orch/auth/errors_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gopher_orch/auth/errors' + +RSpec.describe GopherOrch::Auth::ErrorCodes do + describe 'constants' do + it 'defines SUCCESS as 0' do + expect(described_class::SUCCESS).to eq(0) + end + + it 'defines INVALID_TOKEN as -1000' do + expect(described_class::INVALID_TOKEN).to eq(-1000) + end + + it 'defines EXPIRED_TOKEN as -1001' do + expect(described_class::EXPIRED_TOKEN).to eq(-1001) + end + + it 'defines INVALID_SIGNATURE as -1002' do + expect(described_class::INVALID_SIGNATURE).to eq(-1002) + end + + it 'defines INVALID_ISSUER as -1003' do + expect(described_class::INVALID_ISSUER).to eq(-1003) + end + + it 'defines INVALID_AUDIENCE as -1004' do + expect(described_class::INVALID_AUDIENCE).to eq(-1004) + end + + it 'defines INSUFFICIENT_SCOPE as -1005' do + expect(described_class::INSUFFICIENT_SCOPE).to eq(-1005) + end + + it 'defines JWKS_FETCH_FAILED as -1006' do + expect(described_class::JWKS_FETCH_FAILED).to eq(-1006) + end + + it 'defines INVALID_KEY as -1007' do + expect(described_class::INVALID_KEY).to eq(-1007) + end + + it 'defines NETWORK_ERROR as -1008' do + expect(described_class::NETWORK_ERROR).to eq(-1008) + end + + it 'defines INVALID_CONFIG as -1009' do + expect(described_class::INVALID_CONFIG).to eq(-1009) + end + + it 'defines NOT_INITIALIZED as -1012' do + expect(described_class::NOT_INITIALIZED).to eq(-1012) + end + + it 'defines INTERNAL_ERROR as -1013' do + expect(described_class::INTERNAL_ERROR).to eq(-1013) + end + end + + describe '.description' do + it 'returns description for SUCCESS' do + expect(described_class.description(described_class::SUCCESS)).to eq('Success') + end + + it 'returns description for INVALID_TOKEN' do + expect(described_class.description(described_class::INVALID_TOKEN)).to eq('Invalid token format or structure') + end + + it 'returns description for EXPIRED_TOKEN' do + expect(described_class.description(described_class::EXPIRED_TOKEN)).to eq('Token has expired') + end + + it 'returns description for INVALID_SIGNATURE' do + expect(described_class.description(described_class::INVALID_SIGNATURE)).to eq('Token signature verification failed') + end + + it 'returns description for INVALID_ISSUER' do + expect(described_class.description(described_class::INVALID_ISSUER)).to eq('Token issuer does not match') + end + + it 'returns description for INVALID_AUDIENCE' do + expect(described_class.description(described_class::INVALID_AUDIENCE)).to eq('Token audience does not match') + end + + it 'returns description for INSUFFICIENT_SCOPE' do + expect(described_class.description(described_class::INSUFFICIENT_SCOPE)).to eq('Token does not have required scopes') + end + + it 'returns description for JWKS_FETCH_FAILED' do + expect(described_class.description(described_class::JWKS_FETCH_FAILED)).to eq('Failed to fetch JWKS') + end + + it 'returns description for NETWORK_ERROR' do + expect(described_class.description(described_class::NETWORK_ERROR)).to eq('Network error') + end + + it 'returns description for NOT_INITIALIZED' do + expect(described_class.description(described_class::NOT_INITIALIZED)).to eq('Auth library not initialized') + end + + it 'returns description for INTERNAL_ERROR' do + expect(described_class.description(described_class::INTERNAL_ERROR)).to eq('Internal error') + end + + it 'returns unknown error message for unrecognized codes' do + expect(described_class.description(-9999)).to eq('Unknown error: -9999') + end + end +end + +RSpec.describe GopherOrch::Auth::AuthError do + describe '#initialize' do + it 'creates an error with code and default message' do + error = described_class.new(GopherOrch::Auth::ErrorCodes::INVALID_TOKEN) + + expect(error.code).to eq(-1000) + expect(error.message).to eq('Invalid token format or structure') + end + + it 'creates an error with code and custom message' do + error = described_class.new(GopherOrch::Auth::ErrorCodes::INVALID_TOKEN, 'Custom error message') + + expect(error.code).to eq(-1000) + expect(error.message).to eq('Custom error message') + end + + it 'is a StandardError' do + error = described_class.new(GopherOrch::Auth::ErrorCodes::SUCCESS) + expect(error).to be_a(StandardError) + end + end + + describe 'raising and catching' do + it 'can be raised and caught' do + expect do + raise described_class, GopherOrch::Auth::ErrorCodes::EXPIRED_TOKEN + end.to raise_error(described_class) do |error| + expect(error.code).to eq(-1001) + expect(error.message).to eq('Token has expired') + end + end + + it 'can be caught as StandardError' do + expect do + raise described_class, GopherOrch::Auth::ErrorCodes::NETWORK_ERROR + end.to raise_error(StandardError) + end + end +end diff --git a/spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb b/spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb new file mode 100644 index 00000000..fc1aceaa --- /dev/null +++ b/spec/gopher_orch/auth/oauth/authorization_server_metadata_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gopher_orch/auth/oauth/authorization_server_metadata' + +RSpec.describe GopherOrch::Auth::OAuth::AuthorizationServerMetadata do + describe '#initialize' do + it 'creates metadata with all attributes' do + metadata = described_class.new( + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + jwks_uri: 'https://auth.example.com/.well-known/jwks.json', + registration_endpoint: 'https://auth.example.com/register', + scopes_supported: %w[openid profile email], + response_types_supported: ['code'], + grant_types_supported: %w[authorization_code refresh_token], + token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post], + code_challenge_methods_supported: ['S256'] + ) + + expect(metadata.issuer).to eq('https://auth.example.com') + expect(metadata.authorization_endpoint).to eq('https://auth.example.com/authorize') + expect(metadata.token_endpoint).to eq('https://auth.example.com/token') + expect(metadata.jwks_uri).to eq('https://auth.example.com/.well-known/jwks.json') + expect(metadata.registration_endpoint).to eq('https://auth.example.com/register') + expect(metadata.scopes_supported).to eq(%w[openid profile email]) + expect(metadata.response_types_supported).to eq(['code']) + expect(metadata.grant_types_supported).to eq(%w[authorization_code refresh_token]) + expect(metadata.token_endpoint_auth_methods_supported).to eq(%w[client_secret_basic client_secret_post]) + expect(metadata.code_challenge_methods_supported).to eq(['S256']) + end + + it 'uses default values when attributes not provided' do + metadata = described_class.new + + expect(metadata.issuer).to eq('') + expect(metadata.authorization_endpoint).to eq('') + expect(metadata.token_endpoint).to eq('') + expect(metadata.jwks_uri).to be_nil + expect(metadata.registration_endpoint).to be_nil + expect(metadata.scopes_supported).to eq([]) + expect(metadata.response_types_supported).to eq([]) + expect(metadata.grant_types_supported).to eq([]) + expect(metadata.token_endpoint_auth_methods_supported).to eq([]) + expect(metadata.code_challenge_methods_supported).to eq([]) + end + end + + describe '#to_h' do + it 'returns hash with required fields' do + metadata = described_class.new( + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token' + ) + + hash = metadata.to_h + + expect(hash[:issuer]).to eq('https://auth.example.com') + expect(hash[:authorization_endpoint]).to eq('https://auth.example.com/authorize') + expect(hash[:token_endpoint]).to eq('https://auth.example.com/token') + expect(hash[:scopes_supported]).to eq([]) + expect(hash[:response_types_supported]).to eq([]) + expect(hash[:grant_types_supported]).to eq([]) + end + + it 'includes optional fields when present' do + metadata = described_class.new( + issuer: 'https://auth.example.com', + jwks_uri: 'https://auth.example.com/jwks', + registration_endpoint: 'https://auth.example.com/register' + ) + + hash = metadata.to_h + + expect(hash[:jwks_uri]).to eq('https://auth.example.com/jwks') + expect(hash[:registration_endpoint]).to eq('https://auth.example.com/register') + end + + it 'omits nil optional fields' do + metadata = described_class.new(issuer: 'https://auth.example.com') + + hash = metadata.to_h + + expect(hash).not_to have_key(:jwks_uri) + expect(hash).not_to have_key(:registration_endpoint) + end + end + + describe '#to_json' do + it 'returns valid JSON string' do + metadata = described_class.new( + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + scopes_supported: ['openid'] + ) + + json = metadata.to_json + parsed = JSON.parse(json) + + expect(parsed['issuer']).to eq('https://auth.example.com') + expect(parsed['authorization_endpoint']).to eq('https://auth.example.com/authorize') + expect(parsed['scopes_supported']).to eq(['openid']) + end + end +end diff --git a/spec/gopher_orch/auth/oauth/client_registration_response_spec.rb b/spec/gopher_orch/auth/oauth/client_registration_response_spec.rb new file mode 100644 index 00000000..a127a58d --- /dev/null +++ b/spec/gopher_orch/auth/oauth/client_registration_response_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gopher_orch/auth/oauth/client_registration_response' + +RSpec.describe GopherOrch::Auth::OAuth::ClientRegistrationResponse do + describe '#initialize' do + it 'creates response with all attributes' do + response = described_class.new( + client_id: 'client-123', + client_secret: 'secret-456', + client_id_issued_at: 1_700_000_000, + client_secret_expires_at: 1_800_000_000, + redirect_uris: ['https://app.example.com/callback'], + grant_types: %w[authorization_code refresh_token], + response_types: ['code'], + token_endpoint_auth_method: 'client_secret_post' + ) + + expect(response.client_id).to eq('client-123') + expect(response.client_secret).to eq('secret-456') + expect(response.client_id_issued_at).to eq(1_700_000_000) + expect(response.client_secret_expires_at).to eq(1_800_000_000) + expect(response.redirect_uris).to eq(['https://app.example.com/callback']) + expect(response.grant_types).to eq(%w[authorization_code refresh_token]) + expect(response.response_types).to eq(['code']) + expect(response.token_endpoint_auth_method).to eq('client_secret_post') + end + + it 'uses default values when attributes not provided' do + response = described_class.new + + expect(response.client_id).to eq('') + expect(response.client_secret).to be_nil + expect(response.client_id_issued_at).to eq(0) + expect(response.client_secret_expires_at).to eq(0) + expect(response.redirect_uris).to eq([]) + expect(response.grant_types).to eq([]) + expect(response.response_types).to eq([]) + expect(response.token_endpoint_auth_method).to eq('none') + end + + it 'allows partial initialization for public clients' do + response = described_class.new( + client_id: 'public-client', + redirect_uris: ['https://app.example.com/callback'], + token_endpoint_auth_method: 'none' + ) + + expect(response.client_id).to eq('public-client') + expect(response.client_secret).to be_nil + expect(response.token_endpoint_auth_method).to eq('none') + end + end + + describe '#to_h' do + it 'returns hash with required fields' do + response = described_class.new( + client_id: 'client-123', + client_id_issued_at: 1_700_000_000, + redirect_uris: ['https://app.example.com/callback'] + ) + + hash = response.to_h + + expect(hash[:client_id]).to eq('client-123') + expect(hash[:client_id_issued_at]).to eq(1_700_000_000) + expect(hash[:client_secret_expires_at]).to eq(0) + expect(hash[:redirect_uris]).to eq(['https://app.example.com/callback']) + expect(hash[:grant_types]).to eq([]) + expect(hash[:response_types]).to eq([]) + expect(hash[:token_endpoint_auth_method]).to eq('none') + end + + it 'includes client_secret when present' do + response = described_class.new( + client_id: 'client-123', + client_secret: 'secret-456' + ) + + hash = response.to_h + + expect(hash[:client_secret]).to eq('secret-456') + end + + it 'omits client_secret when nil' do + response = described_class.new(client_id: 'client-123') + + hash = response.to_h + + expect(hash).not_to have_key(:client_secret) + end + end + + describe '#to_json' do + it 'returns valid JSON string' do + response = described_class.new( + client_id: 'client-123', + client_secret: 'secret-456', + redirect_uris: ['https://app.example.com/callback'] + ) + + json = response.to_json + parsed = JSON.parse(json) + + expect(parsed['client_id']).to eq('client-123') + expect(parsed['client_secret']).to eq('secret-456') + expect(parsed['redirect_uris']).to eq(['https://app.example.com/callback']) + end + + it 'omits client_secret from JSON when nil' do + response = described_class.new(client_id: 'public-client') + + json = response.to_json + parsed = JSON.parse(json) + + expect(parsed).not_to have_key('client_secret') + end + end + + describe 'attribute accessors' do + it 'allows modifying attributes after creation' do + response = described_class.new + + response.client_id = 'new-client' + response.client_secret = 'new-secret' + response.redirect_uris = ['https://new.example.com/callback'] + + expect(response.client_id).to eq('new-client') + expect(response.client_secret).to eq('new-secret') + expect(response.redirect_uris).to eq(['https://new.example.com/callback']) + end + end +end diff --git a/spec/gopher_orch/auth/oauth/openid_configuration_spec.rb b/spec/gopher_orch/auth/oauth/openid_configuration_spec.rb new file mode 100644 index 00000000..f3ccb4eb --- /dev/null +++ b/spec/gopher_orch/auth/oauth/openid_configuration_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gopher_orch/auth/oauth/openid_configuration' + +RSpec.describe GopherOrch::Auth::OAuth::OpenIdConfiguration do + describe 'inheritance' do + it 'inherits from AuthorizationServerMetadata' do + expect(described_class.superclass).to eq(GopherOrch::Auth::OAuth::AuthorizationServerMetadata) + end + end + + describe '#initialize' do + it 'creates configuration with all attributes' do + config = described_class.new( + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + jwks_uri: 'https://auth.example.com/jwks', + userinfo_endpoint: 'https://auth.example.com/userinfo', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: %w[RS256 ES256] + ) + + # Parent attributes + expect(config.issuer).to eq('https://auth.example.com') + expect(config.authorization_endpoint).to eq('https://auth.example.com/authorize') + expect(config.token_endpoint).to eq('https://auth.example.com/token') + expect(config.jwks_uri).to eq('https://auth.example.com/jwks') + + # OIDC-specific attributes + expect(config.userinfo_endpoint).to eq('https://auth.example.com/userinfo') + expect(config.subject_types_supported).to eq(['public']) + expect(config.id_token_signing_alg_values_supported).to eq(%w[RS256 ES256]) + end + + it 'uses default values for OIDC fields when not provided' do + config = described_class.new(issuer: 'https://auth.example.com') + + expect(config.userinfo_endpoint).to be_nil + expect(config.subject_types_supported).to eq([]) + expect(config.id_token_signing_alg_values_supported).to eq([]) + end + end + + describe '#to_h' do + it 'includes parent and OIDC fields' do + config = described_class.new( + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + userinfo_endpoint: 'https://auth.example.com/userinfo', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'] + ) + + hash = config.to_h + + # Parent fields + expect(hash[:issuer]).to eq('https://auth.example.com') + expect(hash[:authorization_endpoint]).to eq('https://auth.example.com/authorize') + expect(hash[:token_endpoint]).to eq('https://auth.example.com/token') + + # OIDC fields + expect(hash[:userinfo_endpoint]).to eq('https://auth.example.com/userinfo') + expect(hash[:subject_types_supported]).to eq(['public']) + expect(hash[:id_token_signing_alg_values_supported]).to eq(['RS256']) + end + + it 'omits nil userinfo_endpoint' do + config = described_class.new(issuer: 'https://auth.example.com') + + hash = config.to_h + + expect(hash).not_to have_key(:userinfo_endpoint) + end + + it 'includes empty arrays for required OIDC fields' do + config = described_class.new(issuer: 'https://auth.example.com') + + hash = config.to_h + + expect(hash[:subject_types_supported]).to eq([]) + expect(hash[:id_token_signing_alg_values_supported]).to eq([]) + end + end + + describe '#to_json' do + it 'returns valid JSON with all fields' do + config = described_class.new( + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + userinfo_endpoint: 'https://auth.example.com/userinfo', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'] + ) + + json = config.to_json + parsed = JSON.parse(json) + + expect(parsed['issuer']).to eq('https://auth.example.com') + expect(parsed['userinfo_endpoint']).to eq('https://auth.example.com/userinfo') + expect(parsed['subject_types_supported']).to eq(['public']) + expect(parsed['id_token_signing_alg_values_supported']).to eq(['RS256']) + end + end + + describe 'attribute accessors' do + it 'allows modifying OIDC-specific attributes' do + config = described_class.new + + config.userinfo_endpoint = 'https://auth.example.com/userinfo' + config.subject_types_supported = %w[public pairwise] + config.id_token_signing_alg_values_supported = %w[RS256 RS384] + + expect(config.userinfo_endpoint).to eq('https://auth.example.com/userinfo') + expect(config.subject_types_supported).to eq(%w[public pairwise]) + expect(config.id_token_signing_alg_values_supported).to eq(%w[RS256 RS384]) + end + end +end diff --git a/spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb b/spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb new file mode 100644 index 00000000..2a87d68b --- /dev/null +++ b/spec/gopher_orch/auth/oauth/protected_resource_metadata_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gopher_orch/auth/oauth/protected_resource_metadata' + +RSpec.describe GopherOrch::Auth::OAuth::ProtectedResourceMetadata do + describe '#initialize' do + it 'creates metadata with all attributes' do + metadata = described_class.new( + resource: 'https://api.example.com/mcp', + authorization_servers: ['https://auth.example.com'], + scopes_supported: %w[read write admin], + bearer_methods_supported: %w[header query], + resource_documentation: 'https://docs.example.com' + ) + + expect(metadata.resource).to eq('https://api.example.com/mcp') + expect(metadata.authorization_servers).to eq(['https://auth.example.com']) + expect(metadata.scopes_supported).to eq(%w[read write admin]) + expect(metadata.bearer_methods_supported).to eq(%w[header query]) + expect(metadata.resource_documentation).to eq('https://docs.example.com') + end + + it 'uses default values when attributes not provided' do + metadata = described_class.new + + expect(metadata.resource).to eq('') + expect(metadata.authorization_servers).to eq([]) + expect(metadata.scopes_supported).to eq([]) + expect(metadata.bearer_methods_supported).to eq([]) + expect(metadata.resource_documentation).to be_nil + end + + it 'allows partial initialization' do + metadata = described_class.new( + resource: 'https://api.example.com/mcp', + scopes_supported: ['read'] + ) + + expect(metadata.resource).to eq('https://api.example.com/mcp') + expect(metadata.authorization_servers).to eq([]) + expect(metadata.scopes_supported).to eq(['read']) + end + end + + describe '#to_h' do + it 'returns hash with all required fields' do + metadata = described_class.new( + resource: 'https://api.example.com/mcp', + authorization_servers: ['https://auth.example.com'], + scopes_supported: %w[read write], + bearer_methods_supported: ['header'] + ) + + hash = metadata.to_h + + expect(hash[:resource]).to eq('https://api.example.com/mcp') + expect(hash[:authorization_servers]).to eq(['https://auth.example.com']) + expect(hash[:scopes_supported]).to eq(%w[read write]) + expect(hash[:bearer_methods_supported]).to eq(['header']) + expect(hash).not_to have_key(:resource_documentation) + end + + it 'includes resource_documentation when present' do + metadata = described_class.new( + resource: 'https://api.example.com/mcp', + resource_documentation: 'https://docs.example.com' + ) + + hash = metadata.to_h + + expect(hash[:resource_documentation]).to eq('https://docs.example.com') + end + + it 'omits resource_documentation when nil' do + metadata = described_class.new(resource: 'https://api.example.com/mcp') + + hash = metadata.to_h + + expect(hash).not_to have_key(:resource_documentation) + end + end + + describe '#to_json' do + it 'returns valid JSON string' do + metadata = described_class.new( + resource: 'https://api.example.com/mcp', + authorization_servers: ['https://auth.example.com'], + scopes_supported: ['read'], + bearer_methods_supported: ['header'] + ) + + json = metadata.to_json + parsed = JSON.parse(json) + + expect(parsed['resource']).to eq('https://api.example.com/mcp') + expect(parsed['authorization_servers']).to eq(['https://auth.example.com']) + expect(parsed['scopes_supported']).to eq(['read']) + expect(parsed['bearer_methods_supported']).to eq(['header']) + end + + it 'omits nil optional fields from JSON' do + metadata = described_class.new(resource: 'https://api.example.com/mcp') + + json = metadata.to_json + parsed = JSON.parse(json) + + expect(parsed).not_to have_key('resource_documentation') + end + end + + describe 'attribute accessors' do + it 'allows modifying attributes after creation' do + metadata = described_class.new + + metadata.resource = 'https://new-api.example.com' + metadata.authorization_servers = ['https://new-auth.example.com'] + metadata.scopes_supported = ['new-scope'] + metadata.bearer_methods_supported = ['query'] + metadata.resource_documentation = 'https://new-docs.example.com' + + expect(metadata.resource).to eq('https://new-api.example.com') + expect(metadata.authorization_servers).to eq(['https://new-auth.example.com']) + expect(metadata.scopes_supported).to eq(['new-scope']) + expect(metadata.bearer_methods_supported).to eq(['query']) + expect(metadata.resource_documentation).to eq('https://new-docs.example.com') + end + end +end diff --git a/spec/gopher_orch/auth/www_authenticate_spec.rb b/spec/gopher_orch/auth/www_authenticate_spec.rb new file mode 100644 index 00000000..62d457c7 --- /dev/null +++ b/spec/gopher_orch/auth/www_authenticate_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gopher_orch/auth/www_authenticate' + +RSpec.describe GopherOrch::Auth::WwwAuthenticate do + describe '.generate' do + it 'generates a valid WWW-Authenticate header' do + header = described_class.generate( + realm: 'https://api.example.com', + resource_metadata_url: 'https://api.example.com/.well-known/oauth-protected-resource', + scopes: 'read write', + error: 'invalid_token', + description: 'The access token has expired' + ) + + expect(header).to include('Bearer realm="https://api.example.com"') + expect(header).to include('resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"') + expect(header).to include('scope="read write"') + expect(header).to include('error="invalid_token"') + expect(header).to include('error_description="The access token has expired"') + end + + it 'separates parameters with commas' do + header = described_class.generate( + realm: 'test', + resource_metadata_url: 'url', + scopes: 'scope', + error: 'error', + description: 'desc' + ) + + parts = header.split(', ') + expect(parts.length).to eq(5) + end + + it 'handles empty values' do + header = described_class.generate( + realm: '', + resource_metadata_url: '', + scopes: '', + error: '', + description: '' + ) + + expect(header).to include('realm=""') + expect(header).to include('scope=""') + end + end + + describe '.escape' do + it 'returns empty string for nil' do + expect(described_class.escape(nil)).to eq('') + end + + it 'returns empty string for empty string' do + expect(described_class.escape('')).to eq('') + end + + it 'returns unchanged string when no special characters' do + expect(described_class.escape('simple text')).to eq('simple text') + end + + it 'escapes double quotes' do + expect(described_class.escape('say "hello"')).to eq('say \\"hello\\"') + end + + it 'escapes backslashes' do + expect(described_class.escape('path\\to\\file')).to eq('path\\\\to\\\\file') + end + + it 'escapes both quotes and backslashes' do + expect(described_class.escape('say \\"hi\\"')).to eq('say \\\\\\"hi\\\\\\"') + end + + it 'handles URLs without escaping' do + url = 'https://example.com/path?query=value' + expect(described_class.escape(url)).to eq(url) + end + end + + describe 'RFC 6750 compliance' do + it 'starts with Bearer scheme' do + header = described_class.generate( + realm: 'test', + resource_metadata_url: 'url', + scopes: 'scope', + error: 'invalid_request', + description: 'Missing token' + ) + + expect(header).to start_with('Bearer ') + end + + it 'uses standard OAuth error codes' do + %w[invalid_request invalid_token insufficient_scope].each do |error_code| + header = described_class.generate( + realm: 'test', + resource_metadata_url: 'url', + scopes: 'scope', + error: error_code, + description: 'Test error' + ) + + expect(header).to include("error=\"#{error_code}\"") + end + end + end +end diff --git a/spec/gopher_orch_spec.rb b/spec/gopher_orch_spec.rb index 0254ea41..2c8c044c 100644 --- a/spec/gopher_orch_spec.rb +++ b/spec/gopher_orch_spec.rb @@ -10,7 +10,7 @@ describe '.available?', :requires_native do it 'returns true when native library is available' do - expect(GopherOrch.available?).to be true + expect(described_class.available?).to be true end end @@ -18,51 +18,51 @@ context 'when native library is not available' do before do # Skip if library is actually available - skip 'Library is available' if GopherOrch.available? + skip 'Library is available' if described_class.available? end it 'raises LibraryError' do - expect { GopherOrch.init! }.to raise_error(GopherOrch::LibraryError) + expect { described_class.init! }.to raise_error(GopherOrch::LibraryError) end end end describe '.initialized?', :requires_native do it 'returns true after init!' do - GopherOrch.init! - expect(GopherOrch.initialized?).to be true + described_class.init! + expect(described_class.initialized?).to be true end end describe '.shutdown', :requires_native do it 'sets initialized to false' do - GopherOrch.init! - expect(GopherOrch.initialized?).to be true + described_class.init! + expect(described_class.initialized?).to be true - GopherOrch.shutdown - expect(GopherOrch.initialized?).to be false + described_class.shutdown + expect(described_class.initialized?).to be false end it 'allows re-initialization' do - GopherOrch.init! - GopherOrch.shutdown - GopherOrch.init! - expect(GopherOrch.initialized?).to be true + described_class.init! + described_class.shutdown + described_class.init! + expect(described_class.initialized?).to be true end end describe '.last_error', :requires_native do it 'returns a string or nil' do - GopherOrch.init! - error = GopherOrch.last_error + described_class.init! + error = described_class.last_error expect(error).to be_nil.or be_a(String) end end describe '.clear_error', :requires_native do it 'does not raise an error' do - GopherOrch.init! - expect { GopherOrch.clear_error }.not_to raise_error + described_class.init! + expect { described_class.clear_error }.not_to raise_error end end end diff --git a/spec/native_spec.rb b/spec/native_spec.rb index f313c336..f11106e6 100644 --- a/spec/native_spec.rb +++ b/spec/native_spec.rb @@ -24,43 +24,43 @@ describe '.init!', :requires_native do it 'initializes successfully' do - expect { GopherOrch::Native.init! }.not_to raise_error - expect(GopherOrch::Native.initialized?).to be true + expect { described_class.init! }.not_to raise_error + expect(described_class.initialized?).to be true end end describe '.library_path', :requires_native do it 'returns the library path after initialization' do - GopherOrch::Native.init! - expect(GopherOrch::Native.library_path).to be_a(String) - expect(GopherOrch::Native.library_path).not_to be_empty + described_class.init! + expect(described_class.library_path).to be_a(String) + expect(described_class.library_path).not_to be_empty end end describe '.agent_create_by_json', :requires_native do it 'creates an agent with valid config' do - handle = GopherOrch::Native.agent_create_by_json( + handle = described_class.agent_create_by_json( 'AnthropicProvider', 'claude-3-haiku-20240307', SERVER_CONFIG ) # Agent may be nil if no API key, but function should not crash - GopherOrch::Native.agent_release(handle) if handle && !handle.null? + described_class.agent_release(handle) if handle && !handle.null? # Test passed if we got here without exception expect(true).to be true end it 'handles empty config gracefully' do - handle = GopherOrch::Native.agent_create_by_json( + handle = described_class.agent_create_by_json( 'AnthropicProvider', 'claude-3-haiku-20240307', '{}' ) # Should handle gracefully - GopherOrch::Native.agent_release(handle) if handle && !handle.null? + described_class.agent_release(handle) if handle && !handle.null? # Test passed if we got here without exception expect(true).to be true @@ -69,14 +69,14 @@ describe '.agent_create_by_api_key', :requires_native do it 'handles API key creation' do - handle = GopherOrch::Native.agent_create_by_api_key( + handle = described_class.agent_create_by_api_key( 'AnthropicProvider', 'claude-3-haiku-20240307', 'test-api-key-12345' ) # May return nil if API key is invalid, but should not crash - GopherOrch::Native.agent_release(handle) if handle && !handle.null? + described_class.agent_release(handle) if handle && !handle.null? # Test passed if we got here without exception expect(true).to be true @@ -85,16 +85,16 @@ describe '.last_error', :requires_native do it 'returns a string or nil' do - GopherOrch::Native.init! - error = GopherOrch::Native.last_error + described_class.init! + error = described_class.last_error expect(error).to be_nil.or be_a(String) end end describe '.clear_error', :requires_native do it 'does not raise an error' do - GopherOrch::Native.init! - expect { GopherOrch::Native.clear_error }.not_to raise_error + described_class.init! + expect { described_class.clear_error }.not_to raise_error end end end diff --git a/third_party/gopher-orch b/third_party/gopher-orch new file mode 160000 index 00000000..c8e7c406 --- /dev/null +++ b/third_party/gopher-orch @@ -0,0 +1 @@ +Subproject commit c8e7c40606db330142632ecf90aaa8777bc42a3a diff --git a/third_party/gopher-orch/.clang-format b/third_party/gopher-orch/.clang-format deleted file mode 100644 index bb00198a..00000000 --- a/third_party/gopher-orch/.clang-format +++ /dev/null @@ -1,53 +0,0 @@ ---- -# Google C++ Style Guide -# https://google.github.io/styleguide/cppguide.html -BasedOnStyle: Google -IndentWidth: 2 -ColumnLimit: 80 ---- -Language: Cpp -# Force pointers to the type for C++. -DerivePointerAlignment: false -PointerAlignment: Left -# Other adjustments -AccessModifierOffset: -1 -AllowShortFunctionsOnASingleLine: All -AllowShortIfStatementsOnASingleLine: false -AllowShortLoopsOnASingleLine: false -AlwaysBreakTemplateDeclarations: true -BinPackParameters: false -BreakBeforeBraces: Attach -BreakConstructorInitializers: BeforeColon -ConstructorInitializerAllOnOneLineOrOnePerLine: true -Cpp11BracedListStyle: true -IncludeBlocks: Regroup -IncludeCategories: - # Standard library headers - - Regex: '^<[^/]+>$' - Priority: 1 - # Other library headers - - Regex: '^<.+>$' - Priority: 2 - # Project headers with quotes - - Regex: '^"mcp/.+"$' - Priority: 3 - # Other project headers - - Regex: '^".+"$' - Priority: 4 -IndentCaseLabels: true -KeepEmptyLinesAtTheStartOfBlocks: false -NamespaceIndentation: None -SortIncludes: true -SpaceAfterCStyleCast: false -SpaceAfterTemplateKeyword: true -SpaceBeforeAssignmentOperators: true -SpaceBeforeParens: ControlStatements -SpaceInEmptyParentheses: false -SpacesInAngles: false -SpacesInCStyleCastParentheses: false -SpacesInParentheses: false -SpacesInSquareBrackets: false -Standard: c++14 -UseTab: Never -# Remove trailing whitespace -InsertTrailingCommas: None \ No newline at end of file diff --git a/third_party/gopher-orch/.github/workflows/pr-format-check.yml b/third_party/gopher-orch/.github/workflows/pr-format-check.yml deleted file mode 100644 index 769b322b..00000000 --- a/third_party/gopher-orch/.github/workflows/pr-format-check.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: PR Format Check - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - clang-format: - name: Clang Format - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - steps: - - name: Checkout PR - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - - - name: Install clang-format - run: | - sudo apt-get update - sudo apt-get install -y clang-format-14 - - - name: Check changed files - id: changed-files - run: | - # Get list of changed C/C++ files (excluding submodules) - git diff --name-only origin/${{ github.base_ref }}...HEAD | \ - grep -E '\.(h|hpp|c|cc|cpp)$' | \ - grep -v '^third_party/' > changed_files.txt || true - - if [ -s changed_files.txt ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "Changed C/C++ files:" - cat changed_files.txt - else - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "No C/C++ files changed" - fi - - - name: Check formatting of changed files - if: steps.changed-files.outputs.has_changes == 'true' - run: | - exit_code=0 - while IFS= read -r file; do - if [ -f "$file" ]; then - echo "Checking $file..." - clang-format-14 --style=file --dry-run --Werror "$file" || { - echo "::error file=$file::File is not properly formatted" - exit_code=1 - } - fi - done < changed_files.txt - - if [ $exit_code -ne 0 ]; then - echo "" - echo "::error::Some files are not properly formatted." - echo "To fix, run: make format" - exit 1 - fi - - - name: Post PR comment on failure - if: failure() && steps.changed-files.outputs.has_changes == 'true' - uses: actions/github-script@v7 - with: - script: | - const comment = `## Code Formatting Check Failed - - Some files in this PR are not properly formatted according to the project's clang-format rules. - - **To fix this issue:** - \`\`\`bash - make format - \`\`\` - - Then commit and push the changes.`; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment - }); diff --git a/third_party/gopher-orch/.gitignore b/third_party/gopher-orch/.gitignore deleted file mode 100644 index f8dc7919..00000000 --- a/third_party/gopher-orch/.gitignore +++ /dev/null @@ -1,112 +0,0 @@ -# Build directories -build/ -build-*/ -build_*/ -cmake-build-*/ -out/ -bin/ -lib/ -# Exception: Allow Ruby SDK lib directory -!sdk/ruby/lib/ - -# CMake generated files -CMakeCache.txt -CMakeFiles/ -cmake_install.cmake -CTestTestfile.cmake -Testing/ -_deps/ -# Note: We have a hand-written Makefile at root, so only ignore generated ones in subdirs -*/Makefile - -# Compiled object files -*.o -*.obj -*.lo -*.slo - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app -test_variant -test_variant_advanced -test_variant_extensive -test_optional -test_optional_advanced -test_optional_extensive -test_type_helpers -test_mcp_types -test_mcp_types_extended -test_mcp_type_helpers -test_compat -test_buffer -test_json -test_event_loop -test_io_socket_handle -test_address -test_socket -test_socket_interface -test_socket_option - -# IDE specific files -.vscode/ -.idea/ -*.swp -*.swo -*~ -.DS_Store - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Dependency directories -node_modules/ -vendor/ - -# Coverage files -*.gcov -*.gcda -*.gcno -coverage/ -*.info - -# Documentation -docs/html/ -docs/latex/ -doxygen/ - -# Temporary files -*.tmp -*.temp -*.log - -# Python cache (if using Python scripts) -__pycache__/ -*.py[cod] -*$py.class - -# OS generated files -Thumbs.db -Desktop.ini -# TypeScript compiled output -examples/sdk/typescript/dist/ -sdk/typescript/dist/ diff --git a/third_party/gopher-orch/.gitmodules b/third_party/gopher-orch/.gitmodules deleted file mode 100644 index 06dba06b..00000000 --- a/third_party/gopher-orch/.gitmodules +++ /dev/null @@ -1,5 +0,0 @@ -[submodule "third_party/gopher-mcp"] - path = third_party/gopher-mcp - url = https://github.com/GopherSecurity/gopher-mcp.git - branch = dev_improve_client_and_server - update = none diff --git a/third_party/gopher-orch/CMakeLists.txt b/third_party/gopher-orch/CMakeLists.txt deleted file mode 100644 index b9638ce8..00000000 --- a/third_party/gopher-orch/CMakeLists.txt +++ /dev/null @@ -1,281 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(gopher-orch VERSION 0.1.0 LANGUAGES C CXX) - -# Prevent in-source builds -if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) - message(FATAL_ERROR "In-source builds are not allowed. Please create a build directory and run cmake from there.") -endif() - -# Set C++ standard -set(CMAKE_CXX_STANDARD 14) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) -message(STATUS "Using C++14") - -# Default to Release build for better performance -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) -endif() - -# Build options -option(BUILD_SHARED_LIBS "Build shared libraries" ON) -option(BUILD_STATIC_LIBS "Build static libraries" ON) -option(BUILD_TESTS "Build tests" ON) -option(BUILD_EXAMPLES "Build examples" ON) -option(ORCH_STRICT_WARNINGS "Enable strict compiler warnings" OFF) -option(USE_SUBMODULE_GOPHER_MCP "Use gopher-mcp as submodule (vs find_package)" ON) -option(BUILD_WITHOUT_GOPHER_MCP "Build without gopher-mcp dependency (for testing)" OFF) -option(BUILD_API_PRODUCT "Build for production API environment" OFF) - -# Set output directories -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - -# Compiler flags -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - add_compile_options(-g -O0) - add_compile_definitions(_DEBUG) -elseif(CMAKE_BUILD_TYPE STREQUAL "Release") - add_compile_options(-O3) - add_compile_definitions(NDEBUG) -endif() - -# Configure API environment -if(BUILD_API_PRODUCT) - add_compile_definitions(BUILD_API_PRODUCT=1) - message(STATUS "Configuring for production API environment") -else() - add_compile_definitions(BUILD_API_PRODUCT=0) - message(STATUS "Configuring for test API environment") -endif() - -# Platform-specific settings -if(APPLE) - set(CMAKE_MACOSX_RPATH ON) - set(CMAKE_INSTALL_RPATH "@loader_path/../lib") -elseif(UNIX) - set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib") -endif() - -# Warning flags -if(ORCH_STRICT_WARNINGS) - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - add_compile_options( - -Wall -Wextra -Wpedantic - -Wno-unused-parameter - -Wno-unused-variable - -Wno-unused-function - -Werror - ) - elseif(MSVC) - add_compile_options(/W4 /WX) - endif() -else() - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - add_compile_options( - -Wall - -Wno-unused-parameter - -Wno-unused-variable - -Wno-unused-function - ) - endif() -endif() - -# Handle gopher-mcp dependency -if(BUILD_WITHOUT_GOPHER_MCP) - # Build without gopher-mcp for testing - message(STATUS "Building without gopher-mcp dependency") - set(GOPHER_MCP_LIBRARIES "") - set(GOPHER_MCP_INCLUDE_DIR "") -elseif(USE_SUBMODULE_GOPHER_MCP) - # Use gopher-mcp as submodule - if(NOT EXISTS "${CMAKE_SOURCE_DIR}/third_party/gopher-mcp/.git") -# message(STATUS "gopher-mcp submodule not found. Initializing...") -# execute_process( -# COMMAND git submodule add https://github.com/GopherSecurity/gopher-mcp.git third_party/gopher-mcp -# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} -# RESULT_VARIABLE GIT_SUBMOD_RESULT -# ) -# if(NOT GIT_SUBMOD_RESULT EQUAL "0") -# # Submodule might already exist, try update -# execute_process( -# COMMAND git submodule update --init --recursive third_party/gopher-mcp -# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} -# RESULT_VARIABLE GIT_SUBMOD_UPDATE_RESULT -# ) -# if(NOT GIT_SUBMOD_UPDATE_RESULT EQUAL "0") -# message(FATAL_ERROR "Failed to initialize gopher-mcp submodule") -# endif() -# endif() - else() -# message(STATUS "Updating gopher-mcp submodule...") -# execute_process( -# COMMAND git submodule update --init --recursive third_party/gopher-mcp -# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} -# ) - endif() - - # Set include directories for gopher-mcp BEFORE adding subdirectory - set(GOPHER_MCP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/third_party/gopher-mcp/include) - - # Temporarily add gopher-mcp include directories before processing subdirectory - # This ensures gopher-mcp can find its own headers when building as submodule - include_directories(${GOPHER_MCP_INCLUDE_DIR}) - - # Disable gopher-mcp tests and examples to speed up build - set(BUILD_TESTS_SAVED ${BUILD_TESTS}) - set(BUILD_EXAMPLES_SAVED ${BUILD_EXAMPLES}) - set(BUILD_TESTS OFF CACHE BOOL "" FORCE) - set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - set(BUILD_BINDINGS_EXAMPLES OFF CACHE BOOL "" FORCE) - - # Disable fmt installation if gopher-mcp uses it - set(FMT_INSTALL OFF CACHE BOOL "Disable fmt installation" FORCE) - - # Force gopher-mcp to build in Release mode for better performance - # This helps avoid crashes and improves stability - set(CMAKE_BUILD_TYPE_SAVED ${CMAKE_BUILD_TYPE}) - set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) - - # Add gopher-mcp subdirectory - add_subdirectory(third_party/gopher-mcp EXCLUDE_FROM_ALL) - - # Restore our build type - set(CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE_SAVED} CACHE STRING "" FORCE) - - # Restore our settings - set(BUILD_TESTS ${BUILD_TESTS_SAVED} CACHE BOOL "" FORCE) - set(BUILD_EXAMPLES ${BUILD_EXAMPLES_SAVED} CACHE BOOL "" FORCE) - - # Make gopher-mcp libraries available - # Use static libraries for tests to avoid duplicate initialization - if(BUILD_TESTS AND TARGET gopher-mcp-static) - set(GOPHER_MCP_LIBRARIES gopher-mcp-static gopher-mcp-event-static) - else() - set(GOPHER_MCP_LIBRARIES gopher-mcp gopher-mcp-event) - endif() - - message(STATUS "Using gopher-mcp from submodule") -else() - # Use system-installed gopher-mcp - find_package(gopher-mcp REQUIRED) - message(STATUS "Using system gopher-mcp: ${gopher-mcp_DIR}") -endif() - -# Find libcurl -find_package(CURL REQUIRED) -if(CURL_FOUND) - message(STATUS "Found CURL: ${CURL_LIBRARIES}") - message(STATUS "CURL include dirs: ${CURL_INCLUDE_DIRS}") -else() - message(FATAL_ERROR "libcurl not found. Please install libcurl development package.") -endif() - -# Include directories -message(STATUS "GOPHER_MCP_INCLUDE_DIR: ${GOPHER_MCP_INCLUDE_DIR}") -include_directories( - ${CMAKE_SOURCE_DIR}/include - ${GOPHER_MCP_INCLUDE_DIR} -) - -# Export compile commands for tools like clangd -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# Find required packages -find_package(Threads REQUIRED) - -# Testing setup -if(BUILD_TESTS) - enable_testing() - include(CTest) - - # Fetch Google Test - include(FetchContent) - - # Prevent Google Test from being installed - set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) - set(INSTALL_GMOCK OFF CACHE BOOL "Disable installation of googlemock" FORCE) - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - - FetchContent_Declare( - googletest - GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG v1.14.0 - CMAKE_ARGS -DINSTALL_GTEST=OFF -DINSTALL_GMOCK=OFF - ) - - FetchContent_MakeAvailable(googletest) - - # Include Google Test and Google Mock - include(GoogleTest) -endif() - -# Add subdirectories -add_subdirectory(src) - -if(BUILD_TESTS) - add_subdirectory(tests) -endif() - -if(BUILD_EXAMPLES) - add_subdirectory(examples) -endif() - -# Installation rules -install(DIRECTORY include/orch - DESTINATION include - COMPONENT development - FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" -) - -# Package configuration -include(CMakePackageConfigHelpers) - -configure_package_config_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/gopher-orch-config.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/gopher-orch-config.cmake" - INSTALL_DESTINATION lib/cmake/gopher-orch -) - -write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/gopher-orch-config-version.cmake" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion -) - -install(FILES - "${CMAKE_CURRENT_BINARY_DIR}/gopher-orch-config.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/gopher-orch-config-version.cmake" - DESTINATION lib/cmake/gopher-orch - COMPONENT development -) - -# Add uninstall target -if(NOT TARGET uninstall) - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" - IMMEDIATE @ONLY - ) - - add_custom_target(uninstall - COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake - ) -endif() - -# Print configuration summary -message(STATUS "") -message(STATUS "=== gopher-orch Configuration Summary ===") -message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") -message(STATUS "C++ Standard: ${CMAKE_CXX_STANDARD}") -message(STATUS "Build shared libs: ${BUILD_SHARED_LIBS}") -message(STATUS "Build static libs: ${BUILD_STATIC_LIBS}") -message(STATUS "Build tests: ${BUILD_TESTS}") -message(STATUS "Build examples: ${BUILD_EXAMPLES}") -message(STATUS "API Product build: ${BUILD_API_PRODUCT}") -message(STATUS "Strict warnings: ${ORCH_STRICT_WARNINGS}") -message(STATUS "Use submodule gopher-mcp: ${USE_SUBMODULE_GOPHER_MCP}") -message(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}") -message(STATUS "==========================================") -message(STATUS "") diff --git a/third_party/gopher-orch/LICENSE b/third_party/gopher-orch/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/third_party/gopher-orch/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/third_party/gopher-orch/Makefile b/third_party/gopher-orch/Makefile deleted file mode 100644 index bbf77bbe..00000000 --- a/third_party/gopher-orch/Makefile +++ /dev/null @@ -1,388 +0,0 @@ -# gopher-orch Makefile -# Consolidates all CMake commands for easy building - -# Build configuration -BUILD_DIR ?= build -BUILD_TYPE ?= Debug -GENERATOR ?= "Unix Makefiles" -PARALLEL_JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - -# Library build options (both by default) -BUILD_STATIC ?= ON -BUILD_SHARED ?= ON - -# CMake options -CMAKE_OPTIONS ?= -VERBOSE ?= 0 - -# Colors for output -RED := \033[0;31m -GREEN := \033[0;32m -YELLOW := \033[1;33m -BLUE := \033[0;34m -NC := \033[0m # No Color - -# Default target -.PHONY: all -all: build test - @echo "$(GREEN)Build and test completed successfully$(NC)" - -# Initialize submodules if any are uninitialized (generic for all submodules) -.PHONY: init-submodules -init-submodules: - @if git submodule status | grep -q '^-'; then \ - echo "$(BLUE)Initializing git submodules...$(NC)"; \ - git submodule update --init --recursive; \ - if [ $$? -ne 0 ]; then \ - echo "$(RED)Failed to initialize submodules$(NC)"; \ - exit 1; \ - fi; \ - echo "$(GREEN)Submodules initialized$(NC)"; \ - fi - -# Configure with CMake -.PHONY: configure -configure: init-submodules - @echo "$(BLUE)Configuring with CMake...$(NC)" - @echo " Build type: $(BUILD_TYPE)" - @echo " Static library: $(BUILD_STATIC)" - @echo " Shared library: $(BUILD_SHARED)" - @mkdir -p $(BUILD_DIR) - @cd $(BUILD_DIR) && cmake .. -G $(GENERATOR) \ - -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DBUILD_STATIC_LIBS=$(BUILD_STATIC) \ - -DBUILD_SHARED_LIBS=$(BUILD_SHARED) \ - $(CMAKE_OPTIONS) - @echo "$(GREEN)Configuration complete$(NC)" - -# Build the project -.PHONY: build -build: configure - @echo "$(BLUE)Building gopher-orch libraries...$(NC)" - @cmake --build $(BUILD_DIR) -- -j$(PARALLEL_JOBS) - @echo "$(GREEN)Build complete$(NC)" - @$(MAKE) --no-print-directory lib-info-summary - -# Build in release mode -.PHONY: release -release: - @echo "$(BLUE)Building in Release mode...$(NC)" - @$(MAKE) BUILD_TYPE=Release build test - @echo "$(GREEN)Release build complete$(NC)" - -# Build in debug mode (explicit) -.PHONY: debug -debug: - @echo "$(BLUE)Building in Debug mode...$(NC)" - @$(MAKE) BUILD_TYPE=Debug build - @echo "$(GREEN)Debug build complete$(NC)" - -# Run tests -.PHONY: test -test: build - @echo "$(BLUE)Running tests...$(NC)" - @cd $(BUILD_DIR) && ctest --output-on-failure - @echo "$(GREEN)All tests passed$(NC)" - -# Run tests with verbose output -.PHONY: test-verbose -test-verbose: build - @echo "$(BLUE)Running tests (verbose)...$(NC)" - @cd $(BUILD_DIR) && ctest -V - @echo "$(GREEN)All tests passed$(NC)" - -# Run tests in parallel -.PHONY: test-parallel -test-parallel: build - @echo "$(BLUE)Running tests in parallel...$(NC)" - @cd $(BUILD_DIR) && ctest -j$(PARALLEL_JOBS) --output-on-failure - @echo "$(GREEN)All tests passed$(NC)" - -# Run specific test -.PHONY: test-one -test-one: build - @if [ -z "$(TEST)" ]; then \ - echo "$(RED)Error: TEST variable not set. Usage: make test-one TEST=test_name$(NC)"; \ - exit 1; \ - fi - @echo "$(BLUE)Running test: $(TEST)...$(NC)" - @cd $(BUILD_DIR) && ctest -R $(TEST) -V - @echo "$(GREEN)Test complete$(NC)" - -# Build only the libraries (respects current configuration) -.PHONY: libs -libs: configure - @echo "$(BLUE)Building libraries...$(NC)" - @if [ -f $(BUILD_DIR)/CMakeCache.txt ]; then \ - if grep -q "BUILD_STATIC_LIBS:BOOL=ON" $(BUILD_DIR)/CMakeCache.txt 2>/dev/null; then \ - cmake --build $(BUILD_DIR) --target gopher-orch-static -- -j$(PARALLEL_JOBS); \ - fi; \ - if grep -q "BUILD_SHARED_LIBS:BOOL=ON" $(BUILD_DIR)/CMakeCache.txt 2>/dev/null; then \ - cmake --build $(BUILD_DIR) --target gopher-orch-shared -- -j$(PARALLEL_JOBS); \ - fi; \ - else \ - cmake --build $(BUILD_DIR) --target gopher-orch-static -- -j$(PARALLEL_JOBS); \ - fi - @echo "$(GREEN)Libraries built$(NC)" - -# Build only the examples -.PHONY: examples -examples: libs - @echo "$(BLUE)Building examples...$(NC)" - @cmake --build $(BUILD_DIR) --target hello_world_example -- -j$(PARALLEL_JOBS) - @echo "$(GREEN)Examples built$(NC)" - -# Run the hello world example -.PHONY: run-hello -run-hello: examples - @echo "$(BLUE)Running hello_world_example...$(NC)" - @$(BUILD_DIR)/bin/hello_world_example - @echo "$(GREEN)Example completed$(NC)" - -# Clean build directory -.PHONY: clean -clean: - @echo "$(YELLOW)Cleaning build directory...$(NC)" - @rm -rf $(BUILD_DIR) - @echo "$(GREEN)Clean complete$(NC)" - -# Deep clean (including submodules) -.PHONY: distclean -distclean: clean - @echo "$(YELLOW)Deep cleaning...$(NC)" - @git submodule deinit -f . - @rm -rf third_party/gopher-mcp - @rm -rf .git/modules/third_party - @echo "$(GREEN)Deep clean complete$(NC)" - -# Format all source files -.PHONY: format -format: - @echo "$(BLUE)Formatting all source files with clang-format...$(NC)" - @find . -path "./$(BUILD_DIR)*" -prune -o -path "./third_party" -prune -o \ - \( -name "*.h" -o -name "*.hpp" -o -name "*.cpp" -o -name "*.cc" -o -name "*.c" \) -print | \ - xargs clang-format -i - @echo "$(GREEN)Formatting complete$(NC)" - -# Check formatting without modifying files -.PHONY: check-format -check-format: - @echo "$(BLUE)Checking source file formatting...$(NC)" - @find . -path "./$(BUILD_DIR)*" -prune -o -path "./third_party" -prune -o \ - \( -name "*.h" -o -name "*.hpp" -o -name "*.cpp" -o -name "*.cc" -o -name "*.c" \) -print | \ - xargs clang-format --dry-run --Werror - @if [ $$? -eq 0 ]; then \ - echo "$(GREEN)All files are properly formatted$(NC)"; \ - else \ - echo "$(RED)Format check failed - run 'make format' to fix$(NC)"; \ - exit 1; \ - fi - -# Alias for consistency with gopher-mcp -.PHONY: format-check -format-check: check-format - -# Install the library -.PHONY: install -install: build - @echo "$(BLUE)Installing gopher-orch...$(NC)" - @cmake --build $(BUILD_DIR) --target install - @echo "$(GREEN)Installation complete$(NC)" - -# Uninstall the library -.PHONY: uninstall -uninstall: - @echo "$(YELLOW)Uninstalling gopher-orch...$(NC)" - @if [ ! -f $(BUILD_DIR)/install_manifest.txt ]; then \ - echo "$(RED)Error: No installation found. Run 'make install' first.$(NC)"; \ - exit 1; \ - fi - @cmake --build $(BUILD_DIR) --target uninstall - @echo "$(GREEN)Uninstall complete$(NC)" - -# Generate documentation (requires doxygen) -.PHONY: docs -docs: - @echo "$(BLUE)Generating documentation...$(NC)" - @doxygen Doxyfile 2>/dev/null || echo "$(YELLOW)Warning: Doxygen not found or configured$(NC)" - @echo "$(GREEN)Documentation generated$(NC)" - -# Update submodules -.PHONY: update-submodules -update-submodules: - @echo "$(BLUE)Updating submodules...$(NC)" - @git submodule update --init --recursive - @echo "$(GREEN)Submodules updated$(NC)" - -# Configure to use system gopher-mcp instead of submodule -.PHONY: use-system-gopher-mcp -use-system-gopher-mcp: - @echo "$(BLUE)Configuring to use system gopher-mcp...$(NC)" - @$(MAKE) CMAKE_OPTIONS="-DUSE_SUBMODULE_GOPHER_MCP=OFF" configure - @echo "$(GREEN)Configured to use system gopher-mcp$(NC)" - -# Configure to use submodule gopher-mcp -.PHONY: use-submodule-gopher-mcp -use-submodule-gopher-mcp: - @echo "$(BLUE)Configuring to use submodule gopher-mcp...$(NC)" - @$(MAKE) CMAKE_OPTIONS="-DUSE_SUBMODULE_GOPHER_MCP=ON" configure - @echo "$(GREEN)Configured to use submodule gopher-mcp$(NC)" - -# Build shared library only -.PHONY: shared -shared: - @$(MAKE) BUILD_STATIC=OFF BUILD_SHARED=ON clean build - -# Build static library only -.PHONY: static -static: - @$(MAKE) BUILD_STATIC=ON BUILD_SHARED=OFF clean build - -# Build both static and shared libraries (default behavior) -.PHONY: both -both: - @$(MAKE) BUILD_STATIC=ON BUILD_SHARED=ON clean build - -# Build standalone (without gopher-mcp dependency) -.PHONY: standalone -standalone: - @echo "$(BLUE)Building standalone (without gopher-mcp)...$(NC)" - @$(MAKE) CMAKE_OPTIONS="-DBUILD_WITHOUT_GOPHER_MCP=ON" build - @echo "$(GREEN)Standalone build complete$(NC)" - -# Show brief library summary (used after build) -.PHONY: lib-info-summary -lib-info-summary: - @if [ -f $(BUILD_DIR)/lib/libgopher-orch.a ]; then \ - echo " $(GREEN)Static library: $(BUILD_DIR)/lib/libgopher-orch.a ($$(du -h $(BUILD_DIR)/lib/libgopher-orch.a 2>/dev/null | cut -f1))$(NC)"; \ - fi - @if [ -f $(BUILD_DIR)/lib/libgopher-orch.so ]; then \ - echo " $(GREEN)Shared library: $(BUILD_DIR)/lib/libgopher-orch.so ($$(du -h $(BUILD_DIR)/lib/libgopher-orch.so 2>/dev/null | cut -f1))$(NC)"; \ - elif [ -f $(BUILD_DIR)/lib/libgopher-orch.dylib ]; then \ - echo " $(GREEN)Shared library: $(BUILD_DIR)/lib/libgopher-orch.dylib ($$(du -h $(BUILD_DIR)/lib/libgopher-orch.dylib 2>/dev/null | cut -f1))$(NC)"; \ - fi - -# Show detailed library information -.PHONY: lib-info -lib-info: - @echo "$(BLUE)Library Information:$(NC)" - @if [ -f $(BUILD_DIR)/lib/libgopher-orch.a ]; then \ - echo "$(GREEN)Static library:$(NC)"; \ - echo " Path: $(BUILD_DIR)/lib/libgopher-orch.a"; \ - echo " Size: $$(du -h $(BUILD_DIR)/lib/libgopher-orch.a | cut -f1)"; \ - if command -v ar >/dev/null 2>&1; then \ - echo " Objects: $$(ar -t $(BUILD_DIR)/lib/libgopher-orch.a 2>/dev/null | wc -l) files"; \ - fi; \ - else \ - echo "$(YELLOW)Static library not found$(NC)"; \ - fi - @echo "" - @if [ -f $(BUILD_DIR)/lib/libgopher-orch.so ] || [ -f $(BUILD_DIR)/lib/libgopher-orch.dylib ]; then \ - echo "$(GREEN)Shared library:$(NC)"; \ - LIB_PATH=$$(find $(BUILD_DIR)/lib -name "libgopher-orch.so*" -o -name "libgopher-orch.dylib" | head -1); \ - if [ -n "$$LIB_PATH" ]; then \ - echo " Path: $$LIB_PATH"; \ - echo " Size: $$(du -h $$LIB_PATH | cut -f1)"; \ - if command -v ldd >/dev/null 2>&1; then \ - echo " Dependencies:"; \ - ldd $$LIB_PATH | head -5 | sed 's/^/ /'; \ - elif command -v otool >/dev/null 2>&1; then \ - echo " Dependencies:"; \ - otool -L $$LIB_PATH | head -5 | sed 's/^/ /'; \ - fi; \ - fi; \ - else \ - echo "$(YELLOW)Shared library not found$(NC)"; \ - fi - @echo "" - @if [ -f $(BUILD_DIR)/CMakeCache.txt ]; then \ - echo "$(BLUE)Current configuration:$(NC)"; \ - grep -E "^(BUILD_SHARED_LIBS|BUILD_STATIC_LIBS):BOOL=" $(BUILD_DIR)/CMakeCache.txt | sed 's/^/ /'; \ - fi - -# Show build configuration -.PHONY: info -info: - @echo "$(BLUE)Build Configuration:$(NC)" - @echo " Build directory: $(BUILD_DIR)" - @echo " Build type: $(BUILD_TYPE)" - @echo " Generator: $(GENERATOR)" - @echo " Parallel jobs: $(PARALLEL_JOBS)" - @echo " Build static libs: $(BUILD_STATIC)" - @echo " Build shared libs: $(BUILD_SHARED)" - @echo " CMake options: $(CMAKE_OPTIONS)" - @if [ -f $(BUILD_DIR)/CMakeCache.txt ]; then \ - echo "\n$(BLUE)Current CMake cache:$(NC)"; \ - grep -E "^(CMAKE_BUILD_TYPE|BUILD_SHARED_LIBS|BUILD_STATIC_LIBS|USE_SUBMODULE_GOPHER_MCP)" $(BUILD_DIR)/CMakeCache.txt || true; \ - else \ - echo "\n$(YELLOW)No build directory found. Run 'make configure' first.$(NC)"; \ - fi - -# Help target -.PHONY: help -help: - @echo "$(BLUE)gopher-orch Build System$(NC)" - @echo "" - @echo "$(GREEN)Common targets:$(NC)" - @echo " make - Build both libraries and run tests (default)" - @echo " make build - Build both static and shared libraries" - @echo " make release - Build and test in release mode" - @echo " make test - Run tests" - @echo " make clean - Clean build directory" - @echo " make install - Install the libraries" - @echo " make uninstall - Uninstall the libraries" - @echo "" - @echo "$(GREEN)Library build targets:$(NC)" - @echo " make both - Build both library types (default)" - @echo " make static - Build static library only (with clean)" - @echo " make shared - Build shared library only (with clean)" - @echo " make libs - Build libraries (current config)" - @echo " make lib-info - Show detailed library information" - @echo "" - @echo "$(GREEN)Build modes:$(NC)" - @echo " make debug - Build in debug mode" - @echo " make release - Build in release mode" - @echo " make standalone - Build without gopher-mcp" - @echo "" - @echo "$(GREEN)Test targets:$(NC)" - @echo " make test-verbose - Run tests with verbose output" - @echo " make test-parallel - Run tests in parallel" - @echo " make test-one TEST=name - Run specific test" - @echo "" - @echo "$(GREEN)Component targets:$(NC)" - @echo " make libs - Build only libraries" - @echo " make examples - Build examples" - @echo " make run-hello - Run hello world example" - @echo "" - @echo "$(GREEN)Dependency management:$(NC)" - @echo " make init-submodules - Initialize submodules (auto on build)" - @echo " make update-submodules - Update git submodules" - @echo " make use-system-gopher-mcp - Use system gopher-mcp" - @echo " make use-submodule-gopher-mcp - Use submodule gopher-mcp" - @echo "" - @echo "$(GREEN)Code Quality:$(NC)" - @echo " make format - Auto-format all source files" - @echo " make check-format - Check formatting without modifying" - @echo " make format-check - Alias for check-format" - @echo "" - @echo "$(GREEN)Utilities:$(NC)" - @echo " make docs - Generate documentation" - @echo " make info - Show build configuration" - @echo " make distclean - Deep clean including submodules" - @echo "" - @echo "$(GREEN)Variables:$(NC)" - @echo " BUILD_DIR=dir - Set build directory (default: build)" - @echo " BUILD_TYPE=type - Set build type (Debug/Release, default: Debug)" - @echo " BUILD_STATIC=ON/OFF - Build static library (default: ON)" - @echo " BUILD_SHARED=ON/OFF - Build shared library (default: ON)" - @echo " CMAKE_OPTIONS=opts - Additional CMake options" - @echo " PARALLEL_JOBS=n - Number of parallel jobs" - @echo "" - @echo "$(GREEN)Examples:$(NC)" - @echo " make - Build both libraries (default)" - @echo " make BUILD_SHARED=OFF - Build static library only" - @echo " make BUILD_TYPE=Release - Build both libraries in release mode" - @echo " make static - Build only static library" - -.DEFAULT_GOAL := all diff --git a/third_party/gopher-orch/README.md b/third_party/gopher-orch/README.md deleted file mode 100644 index 91c0b0ca..00000000 --- a/third_party/gopher-orch/README.md +++ /dev/null @@ -1,382 +0,0 @@ -# Gopher Orch - Cross-Language MCP Orchestration Framework - -[![MCP](https://img.shields.io/badge/MCP-Native-green.svg)](https://modelcontextprotocol.io/) -[![Languages](https://img.shields.io/badge/C++%20%7C%20Python%20%7C%20Rust%20%7C%20Go%20%7C%20Node.js-blue.svg)]() -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -[![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey.svg)]() - -**LangChain + Vercel AI SDK for Model Context Protocol** - -Build composable AI agents and workflows in **C++, Python, Rust, Go, Node.js, and more** - with MCP built-in. - -## What is Gopher Orch? - -Gopher Orch is a **cross-language MCP orchestration framework** that provides composable building blocks for AI agents and workflows. Built on top of [gopher-mcp](https://github.com/anthropics/gopher-mcp), it enables developers to build ReAct agents, stateful workflows, and multi-step reasoning systems with enterprise-grade reliability - in any language. - -### Key Benefits - -- **MCP-Native**: First-class Model Context Protocol support - tools, resources, prompts built-in -- **Cross-Language**: Write agents in C++, Python, Rust, Go, Node.js, and more with unified API -- **LangChain-Style Composability**: Chain operations with `|` operator, build complex workflows from simple components -- **Vercel AI SDK Patterns**: Streaming, structured outputs, and modern async patterns -- **Production-Ready**: Circuit breaker, retry, timeout, and fallback patterns built-in -- **Testable-by-Design**: MockServer support for unit testing without network dependencies - -## Why Choose Gopher Orch? - -| Feature | Gopher Orch | LangChain | LlamaIndex | -|---------|-------------|-----------|------------| -| Languages | C++, Python, Rust, Go, Node.js, and more | Python | Python | -| MCP Support | Native (built-in) | Plugin | Plugin | -| Performance | Native speed, zero-copy | Interpreted | Interpreted | -| Type Safety | Compile-time checked | Runtime | Runtime | -| Composability | Explicit `Runnable` | Magic methods | Index abstractions | -| Streaming | Built-in | Callback-based | Callback-based | -| Memory Control | RAII, deterministic | GC-managed | GC-managed | - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ AI Agents │ Workflows │ State Graphs │ Chatbots │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────────────┤ -│ FFI Layer (Cross-Language) │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ Python │ Rust │ Go │ Node.js │ Java │ C# │ Ruby │ Swift │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────────────┤ -│ Orchestration Layer │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ -│ │ Runnable │ │ StateGraph │ │ Resilience │ │ Agent │ │ -│ │ Composition │ │ (Pregel) │ │ Patterns │ │ (ReAct) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │ -├─────────────────────────────────────────────────────────────────────┤ -│ Server Abstraction Layer │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ Protocol-Agnostic Server Interface │ Tool Registry │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────────────┤ -│ Protocol Implementations │ -│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ -│ │ MCP Server │ │ REST Server │ │ Mock Server │ │ -│ └────────────────┘ └────────────────┘ └────────────────┘ │ -├─────────────────────────────────────────────────────────────────────┤ -│ Foundation (gopher-mcp) │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ Dispatcher │ JsonValue │ Result │ Event Loop │ Transports │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## Core Components - -### Runnable Interface - Universal Building Block - -The `Runnable` interface is the foundation of all composable operations: - -```cpp -#include "gopher/orch/orch.h" - -using namespace gopher::orch; - -// Create a simple lambda runnable -auto greet = makeLambda( - [](const std::string& name, Dispatcher& d, ResultCallback cb) { - cb(Result("Hello, " + name + "!")); - }); - -// Invoke asynchronously -greet->invoke("World", config, dispatcher, [](Result result) { - std::cout << mcp::get(result) << std::endl; -}); -``` - -### Composition Patterns - -Build complex workflows from simple components: - -```cpp -// Sequence: A | B | C (pipe pattern) -auto pipeline = makeSequence(step1, step2, step3); - -// Parallel: Run operations concurrently -auto parallel = makeParallel({taskA, taskB, taskC}); - -// Router: Conditional branching -auto router = makeRouter() - .addRoute("search", searchHandler) - .addRoute("calculate", calculateHandler) - .withDefault(defaultHandler) - .build(); -``` - -### ReAct Agent - Reasoning + Acting - -Build AI agents that reason about tasks and use tools: - -```cpp -#include "gopher/orch/agent/agent_runnable.h" - -// Create LLM provider -auto provider = makeOpenAIProvider(api_key, "gpt-4"); - -// Create tool registry -auto registry = makeToolRegistry(); -registry->addSyncTool("search", "Search the web", schema, - [](const JsonValue& args) -> Result { - // Tool implementation - return Result(searchResults); - }); - -// Create ReAct agent -auto agent = makeAgentRunnable(provider, registry, - AgentConfig("gpt-4") - .withSystemPrompt("You are a helpful assistant.") - .withMaxIterations(10)); - -// Run agent -JsonValue input = "What's the weather in Tokyo?"; -agent->invoke(input, config, dispatcher, [](Result result) { - auto output = mcp::get(result); - std::cout << output["response"].getString() << std::endl; -}); -``` - -### StateGraph - LangGraph-Style Workflows - -Build stateful workflows with conditional transitions: - -```cpp -#include "gopher/orch/graph/state_graph.h" - -// Define state with reducer -struct AgentState { - std::vector messages; // APPEND reducer - int step_count = 0; // LAST_WRITE_WINS - - static AgentState reduce(const AgentState& a, const AgentState& b); -}; - -// Build graph -auto graph = StateGraphBuilder() - .addNode("agent", agentNode) - .addNode("tools", toolsNode) - .addEdge(START, "agent") - .addConditionalEdge("agent", shouldContinue, { - {"continue", "tools"}, - {"end", END} - }) - .addEdge("tools", "agent") - .compile(); - -// Execute -graph->invoke(initialState, config, dispatcher, callback); -``` - -### Resilience Patterns - -Add production-grade reliability to any runnable: - -```cpp -// Retry with exponential backoff -auto reliable = makeRetry(unreliableOp, RetryConfig() - .withMaxAttempts(3) - .withBackoff(std::chrono::milliseconds(100))); - -// Timeout protection -auto bounded = makeTimeout(slowOp, std::chrono::seconds(30)); - -// Fallback on failure -auto safe = makeFallback(primaryOp, fallbackOp); - -// Circuit breaker for failure isolation -auto protected = makeCircuitBreaker(externalService, CircuitBreakerConfig() - .withFailureThreshold(5) - .withResetTimeout(std::chrono::seconds(60))); -``` - -### LLM Providers - -Built-in support for major LLM providers: - -```cpp -// OpenAI / GPT-4 -auto openai = makeOpenAIProvider(api_key, "gpt-4"); - -// Anthropic / Claude -auto anthropic = makeAnthropicProvider(api_key, "claude-3-opus-20240229"); - -// Use with LLMRunnable for composable LLM operations -auto llm = makeLLMRunnable(provider, LLMConfig() - .withModel("gpt-4") - .withTemperature(0.7)); -``` - -### Protocol-Agnostic Server - -Register tools once, expose via any protocol: - -```cpp -// Create server with tool registry -auto server = makeServer(registry, ServerConfig() - .withName("my-agent-server")); - -// Expose via MCP protocol -auto mcpServer = makeMCPServer(server, mcpConfig); -mcpServer->listen("tcp://0.0.0.0:8080"); - -// Or expose via REST API -auto restServer = makeRESTServer(server, restConfig); -restServer->listen("http://0.0.0.0:3000"); - -// Or use MockServer for testing -auto mockServer = makeMockServer(server); -mockServer->setToolResponse("search", mockResponse); -``` - -## Installation - -### Prerequisites - -- C++14 or later compiler (GCC 8+, Clang 10+, MSVC 2019+) -- CMake 3.10+ -- [gopher-mcp](https://github.com/anthropics/gopher-mcp) (auto-fetched as submodule) - -### Build from Source - -```bash -# Clone with submodules -git clone --recursive https://github.com/anthropics/gopher-orch.git -cd gopher-orch - -# Build -make - -# Run tests -make test - -# Install (auto-prompts for sudo if needed) -make install -``` - -### CMake Integration - -```cmake -# Option 1: FetchContent -include(FetchContent) -FetchContent_Declare( - gopher-orch - GIT_REPOSITORY https://github.com/anthropics/gopher-orch.git - GIT_TAG main -) -FetchContent_MakeAvailable(gopher-orch) - -target_link_libraries(your_target gopher-orch) - -# Option 2: Submodule -add_subdirectory(third_party/gopher-orch) -target_link_libraries(your_target gopher-orch) -``` - -## Use Cases - -### 1. AI Chatbots and Assistants -Build conversational AI agents with tool-calling capabilities, memory, and multi-turn reasoning. - -### 2. Autonomous Agents -Create agents that can break down complex tasks, use tools, and iterate until completion. - -### 3. Workflow Automation -Orchestrate multi-step business processes with conditional branching and error handling. - -### 4. RAG Pipelines -Build retrieval-augmented generation systems with composable retrieval and synthesis steps. - -### 5. Multi-Agent Systems -Coordinate multiple specialized agents working together on complex problems. - -### 6. API Orchestration -Compose multiple API calls with resilience patterns and parallel execution. - -## Cross-Language Support (FFI) - -Gopher Orch provides a stable C API for integration with other languages: - -```python -# Python example -from gopher_orch import Agent, ToolRegistry - -registry = ToolRegistry() -registry.add_tool("search", search_function) - -agent = Agent(provider, registry, config) -result = agent.invoke("What's the weather?") -``` - -Supported languages: -- **Python**: ctypes/cffi with async support -- **Rust**: Safe FFI wrappers -- **Go**: CGO integration -- **Node.js**: N-API bindings -- **Java**: JNI bindings -- **C#/.NET**: P/Invoke - -## Documentation - -- [Runnable Interface](docs/Runnable.md) - Core composable interface -- [Composition Patterns](docs/Composition.md) - Sequence, Parallel, Router -- [Agent Framework](docs/Agent.md) - ReAct agents and tool execution -- [StateGraph Guide](docs/StateGraph.md) - LangGraph-style stateful workflows -- [Resilience Patterns](docs/Resilience.md) - Retry, Timeout, Fallback, Circuit Breaker -- [Server Abstraction](docs/Server.md) - Protocol-agnostic server interface -- [FFI Guide](docs/FFI.md) - Cross-language integration - -## Examples - -See the [examples/](examples/) directory for complete working examples: - -- `examples/simple_agent/` - Basic ReAct agent with tools -- `examples/chatbot/` - Multi-turn conversational agent -- `examples/workflow/` - StateGraph-based workflow -- `examples/resilient_api/` - API client with resilience patterns -- `examples/multi_agent/` - Multi-agent coordination - -## Comparison with Other Frameworks - -### vs LangChain (Python) -- **Performance**: Native C++ vs interpreted Python -- **Type Safety**: Compile-time vs runtime errors -- **Memory**: Deterministic RAII vs garbage collection -- **Design**: Explicit interfaces vs magic methods - -### vs LlamaIndex (Python) -- **Focus**: General orchestration vs RAG-specific -- **Flexibility**: Protocol-agnostic vs LLM-focused -- **Composability**: Universal Runnable vs Index abstractions - -### vs Semantic Kernel (C#/.NET) -- **Language**: C++ with FFI vs .NET ecosystem -- **Portability**: Cross-platform native vs .NET runtime -- **Protocol**: MCP-native vs custom plugins - -## Contributing - -Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) before submitting pull requests. - -## License - -Apache License 2.0 - see [LICENSE](LICENSE) for details. - -## Related Projects - -- [gopher-mcp](https://github.com/anthropics/gopher-mcp) - C++ MCP SDK (foundation layer) -- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification -- [LangChain](https://github.com/langchain-ai/langchain) - Python AI orchestration -- [LlamaIndex](https://github.com/run-llama/llama_index) - Python RAG framework - -## Keywords & Search Terms - -`MCP SDK`, `MCP Framework`, `Model Context Protocol SDK`, `MCP Orchestration`, `MCP Agent`, `Cross-Language AI Agent`, `LangChain for MCP`, `Vercel AI SDK MCP`, `MCP Tools`, `MCP Python`, `MCP Rust`, `MCP Go`, `MCP Node.js`, `AI Agent Framework`, `ReAct Agent MCP`, `LangGraph MCP`, `Agentic AI MCP`, `MCP Server`, `MCP Client`, `Tool Calling MCP`, `AI Workflow MCP` diff --git a/third_party/gopher-orch/build.sh b/third_party/gopher-orch/build.sh deleted file mode 100755 index 3aa8a1ca..00000000 --- a/third_party/gopher-orch/build.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash -x - -# Build script for gopher-orch with submodule support - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}=== gopher-orch Build Script ===${NC}" - -# Parse arguments -BUILD_TYPE="${BUILD_TYPE:-Debug}" -BUILD_DIR="${BUILD_DIR:-build}" -USE_SUBMODULE=ON -BUILD_TESTS=ON -BUILD_EXAMPLES=ON - -for arg in "$@"; do - case $arg in - --release) - BUILD_TYPE=Release - shift - ;; - --no-submodule) - USE_SUBMODULE=OFF - shift - ;; - --no-tests) - BUILD_TESTS=OFF - shift - ;; - --no-examples) - BUILD_EXAMPLES=OFF - shift - ;; - --standalone) - # Build without gopher-mcp for testing - USE_SUBMODULE=OFF - BUILD_WITHOUT_MCP=ON - shift - ;; - --clean) - echo -e "${YELLOW}Cleaning build directory...${NC}" - rm -rf "$BUILD_DIR" - shift - ;; - --help) - echo "Usage: $0 [options]" - echo "Options:" - echo " --release Build in Release mode (default: Debug)" - echo " --no-submodule Use system gopher-mcp instead of submodule" - echo " --no-tests Don't build tests" - echo " --no-examples Don't build examples" - echo " --standalone Build without gopher-mcp dependency" - echo " --clean Clean build directory before building" - echo " --help Show this help message" - exit 0 - ;; - esac -done - -# Initialize submodule if needed -if [ "$USE_SUBMODULE" = "ON" ] && [ "${BUILD_WITHOUT_MCP:-OFF}" = "OFF" ]; then - if [ ! -f "third_party/gopher-mcp/CMakeLists.txt" ]; then - echo -e "${YELLOW}Initializing gopher-mcp submodule...${NC}" - git submodule update --init --recursive third_party/gopher-mcp - else - echo -e "${GREEN}gopher-mcp submodule already initialized${NC}" - fi -fi - -# Create build directory -mkdir -p "$BUILD_DIR" - -# Configure -echo -e "${BLUE}Configuring with CMake...${NC}" -echo " Build type: $BUILD_TYPE" -echo " Use submodule: $USE_SUBMODULE" -echo " Build tests: $BUILD_TESTS" -echo " Build examples: $BUILD_EXAMPLES" - -CMAKE_ARGS=( - -DCMAKE_BUILD_TYPE="$BUILD_TYPE" - -DUSE_SUBMODULE_GOPHER_MCP="$USE_SUBMODULE" - -DBUILD_TESTS="$BUILD_TESTS" - -DBUILD_EXAMPLES="$BUILD_EXAMPLES" -) - -if [ "${BUILD_WITHOUT_MCP:-OFF}" = "ON" ]; then - CMAKE_ARGS+=(-DBUILD_WITHOUT_GOPHER_MCP=ON) - echo -e "${YELLOW}Building without gopher-mcp dependency (standalone mode)${NC}" -fi - -cmake -B "$BUILD_DIR" -S . "${CMAKE_ARGS[@]}" - -# Build -echo -e "${BLUE}Building...${NC}" -cmake --build "$BUILD_DIR" -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - -echo -e "${GREEN}Build completed successfully!${NC}" - -# Run tests if built -if [ "$BUILD_TESTS" = "ON" ]; then - echo -e "${BLUE}Running tests...${NC}" - (cd "$BUILD_DIR" && ctest --output-on-failure) || { - echo -e "${RED}Some tests failed${NC}" - exit 1 - } - echo -e "${GREEN}All tests passed!${NC}" -fi - -# Show example usage -if [ "$BUILD_EXAMPLES" = "ON" ] && [ -f "$BUILD_DIR/bin/hello_world_example" ]; then - echo -e "${BLUE}Example built:${NC}" - echo " Run: ./$BUILD_DIR/bin/hello_world_example" -fi - -echo -e "${GREEN}=== Build Complete ===${NC}" diff --git a/third_party/gopher-orch/cmake/cmake_uninstall.cmake.in b/third_party/gopher-orch/cmake/cmake_uninstall.cmake.in deleted file mode 100644 index 25ae5708..00000000 --- a/third_party/gopher-orch/cmake/cmake_uninstall.cmake.in +++ /dev/null @@ -1,49 +0,0 @@ -# cmake_uninstall.cmake.in -# Uninstall script for gopher-orch - -if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") - message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt") -endif() - -file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files) -string(REGEX REPLACE "\n" ";" files "${files}") - -foreach(file ${files}) - message(STATUS "Uninstalling $ENV{DESTDIR}${file}") - if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") - exec_program( - "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" - OUTPUT_VARIABLE rm_out - RETURN_VALUE rm_retval - ) - if(NOT "${rm_retval}" STREQUAL 0) - message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") - endif() - else() - message(STATUS "File $ENV{DESTDIR}${file} does not exist.") - endif() -endforeach() - -# Remove empty directories -set(DIRS_TO_CHECK - "@CMAKE_INSTALL_PREFIX@/lib/cmake/gopher-orch" - "@CMAKE_INSTALL_PREFIX@/include/orch/core" - "@CMAKE_INSTALL_PREFIX@/include/orch" -) - -foreach(dir ${DIRS_TO_CHECK}) - if(EXISTS "$ENV{DESTDIR}${dir}") - file(GLOB dir_contents "$ENV{DESTDIR}${dir}/*") - list(LENGTH dir_contents n_contents) - if(n_contents EQUAL 0) - message(STATUS "Removing empty directory: $ENV{DESTDIR}${dir}") - exec_program( - "@CMAKE_COMMAND@" ARGS "-E remove_directory \"$ENV{DESTDIR}${dir}\"" - OUTPUT_VARIABLE rm_out - RETURN_VALUE rm_retval - ) - endif() - endif() -endforeach() - -message(STATUS "Uninstall complete") diff --git a/third_party/gopher-orch/cmake/gopher-orch-config.cmake.in b/third_party/gopher-orch/cmake/gopher-orch-config.cmake.in deleted file mode 100644 index 89457eef..00000000 --- a/third_party/gopher-orch/cmake/gopher-orch-config.cmake.in +++ /dev/null @@ -1,23 +0,0 @@ -@PACKAGE_INIT@ - -include(CMakeFindDependencyMacro) - -# Find required dependencies -if(@USE_SUBMODULE_GOPHER_MCP@) - # When gopher-orch was built with submodule, users need gopher-mcp - find_dependency(gopher-mcp REQUIRED) -endif() - -# Find threads -find_dependency(Threads REQUIRED) - -# Include the targets file -include("${CMAKE_CURRENT_LIST_DIR}/gopher-orch-targets.cmake") - -# Set variables for compatibility -set(gopher-orch_FOUND TRUE) -set(gopher-orch_INCLUDE_DIRS "@CMAKE_INSTALL_PREFIX@/include") -set(gopher-orch_LIBRARIES gopher-orch) - -# Check required components -check_required_components(gopher-orch) diff --git a/third_party/gopher-orch/docker/Dockerfile b/third_party/gopher-orch/docker/Dockerfile deleted file mode 100644 index 5b19a056..00000000 --- a/third_party/gopher-orch/docker/Dockerfile +++ /dev/null @@ -1,120 +0,0 @@ -# ═══════════════════════════════════════════════════════════════════════════════ -# MCP Gateway Server - Multi-Stage Docker Build -# ═══════════════════════════════════════════════════════════════════════════════ -# -# This Dockerfile builds a production-ready MCP Gateway Server. -# -# Build: -# docker build -t mcp-gateway -f docker/Dockerfile . -# -# Run: -# docker run -p 3003:3003 \ -# -e MCP_GATEWAY_CONFIG='{"version":"2026-01-11","metadata":{"gatewayId":"123"},"config":{},"servers":[{"serverId":"1","name":"server1","url":"http://host.docker.internal:3001/mcp"}]}' \ -# mcp-gateway -# -# Environment Variables: -# MCP_GATEWAY_CONFIG - JSON config string -# MCP_GATEWAY_CONFIG_PATH - Path to config file (default: /etc/mcp/gateway-config.json) -# MCP_GATEWAY_CONFIG_URL - API URL to fetch config -# MCP_GATEWAY_ACCESS_KEY - Access key for API auth -# MCP_GATEWAY_PORT - Server port (default: 3003) -# MCP_GATEWAY_HOST - Server host (default: 0.0.0.0) -# MCP_GATEWAY_NAME - Server name (default: mcp-gateway) -# -# ═══════════════════════════════════════════════════════════════════════════════ - -# ─────────────────────────────────────────────────────────────────────────────── -# Stage 1: Builder -# ─────────────────────────────────────────────────────────────────────────────── -FROM ubuntu:22.04 AS builder - -# Prevent interactive prompts during package installation -ENV DEBIAN_FRONTEND=noninteractive - -# Install build dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - cmake \ - git \ - pkg-config \ - libcurl4-openssl-dev \ - libssl-dev \ - libevent-dev \ - && rm -rf /var/lib/apt/lists/* - -# Set working directory -WORKDIR /build - -# Copy source code -COPY . . - -# Initialize and update git submodules (for gopher-mcp) -RUN git submodule update --init --recursive || true - -# Configure CMake with Release build -RUN cmake -B build \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_EXAMPLES=OFF \ - -DBUILD_TESTS=OFF \ - -DBUILD_SHARED_LIBS=OFF \ - -DBUILD_STATIC_LIBS=ON - -# Build the mcp_gateway binary -RUN cmake --build build --target mcp_gateway -j$(nproc) - -# Verify binary was built -RUN ls -la build/bin/mcp_gateway - -# ─────────────────────────────────────────────────────────────────────────────── -# Stage 2: Runtime -# ─────────────────────────────────────────────────────────────────────────────── -FROM ubuntu:22.04 AS runtime - -# Prevent interactive prompts -ENV DEBIAN_FRONTEND=noninteractive - -# Install runtime dependencies only -RUN apt-get update && apt-get install -y --no-install-recommends \ - libcurl4 \ - libssl3 \ - libevent-2.1-7 \ - libevent-pthreads-2.1-7 \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get clean - -# Create non-root user for security -RUN groupadd -r -g 1001 mcp && \ - useradd -r -u 1001 -g mcp mcp - -# Create config directory -RUN mkdir -p /etc/mcp && chown mcp:mcp /etc/mcp - -# Copy binary from builder -COPY --from=builder /build/build/bin/mcp_gateway /usr/local/bin/mcp_gateway - -# Make binary executable -RUN chmod +x /usr/local/bin/mcp_gateway - -# Switch to non-root user -USER mcp - -# Set working directory -WORKDIR /app - -# Default environment variables -ENV MCP_GATEWAY_PORT=3003 -ENV MCP_GATEWAY_HOST=0.0.0.0 -ENV MCP_GATEWAY_NAME=mcp-gateway -ENV MCP_GATEWAY_CONFIG_PATH=/etc/mcp/gateway-config.json - -# Expose default port -EXPOSE 3003 - -# Health check -# The gateway exposes /health endpoint -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:${MCP_GATEWAY_PORT}/health || exit 1 - -# Run the gateway -ENTRYPOINT ["/usr/local/bin/mcp_gateway"] diff --git a/third_party/gopher-orch/docker/README.md b/third_party/gopher-orch/docker/README.md deleted file mode 100644 index 9d2fc36c..00000000 --- a/third_party/gopher-orch/docker/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# MCP Gateway Docker - -This directory contains the Docker build infrastructure for the MCP Gateway Server. - -## Files - -| File | Description | -|------|-------------| -| `Dockerfile` | Multi-stage build for production binary | -| `build-and-push.sh` | Script to build and push to AWS ECR | - -## Quick Start - -### Build and Push to ECR - -```bash -./docker/build-and-push.sh -``` - -### Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `AWS_REGION` | `us-east-1` | AWS region for ECR | -| `AWS_ACCOUNT_ID` | `XX308818XX` | AWS account ID | -| `REPOSITORY_NAME` | `mcp-gateway` | ECR repository name | - -### Local Build - -```bash -# Build for local testing -docker build -t mcp-gateway -f docker/Dockerfile . -``` - -### Local Run - -```bash -# With environment variable config (manifest format) -docker run -p 3003:3003 \ - -e MCP_GATEWAY_CONFIG='{"version":"2026-01-11","metadata":{"gatewayId":"123"},"config":{},"servers":[{"serverId":"1","name":"server1","url":"http://host.docker.internal:3001/mcp"}]}' \ - mcp-gateway - -# With config file -docker run -p 3003:3003 \ - -v /path/to/config:/etc/mcp:ro \ - mcp-gateway -``` - -## Container Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `MCP_GATEWAY_CONFIG` | - | JSON configuration string (highest priority) | -| `MCP_GATEWAY_CONFIG_PATH` | `/etc/mcp/gateway-config.json` | Path to config file | -| `MCP_GATEWAY_CONFIG_URL` | - | API URL to fetch configuration | -| `MCP_GATEWAY_ACCESS_KEY` | - | Access key for API authentication | -| `MCP_GATEWAY_PORT` | `3003` | Server listen port | -| `MCP_GATEWAY_HOST` | `0.0.0.0` | Server listen host | -| `MCP_GATEWAY_NAME` | `mcp-gateway` | Server name | - -## Health Check - -The container includes a built-in health check: - -```bash -curl http://localhost:3003/health -``` - -## Image Details - -- **Base Image**: Ubuntu 22.04 -- **Architectures**: `linux/amd64`, `linux/arm64` -- **User**: Non-root (`mcp:mcp`, UID/GID 1001) -- **Port**: 3003 (configurable) - -## Full Documentation - -See [MCP Gateway Deployment Guide](../docs/MCP-Gateway-Deployment.md) for complete deployment instructions. diff --git a/third_party/gopher-orch/docker/build-and-push.sh b/third_party/gopher-orch/docker/build-and-push.sh deleted file mode 100755 index 34edd6cd..00000000 --- a/third_party/gopher-orch/docker/build-and-push.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ═══════════════════════════════════════════════════════════════════════════════ -# MCP Gateway - Build and Push to ECR -# ═══════════════════════════════════════════════════════════════════════════════ -# -# This script builds a multi-architecture Docker image for the MCP Gateway -# and pushes it to AWS ECR. -# -# Usage: -# ./docker/build-and-push.sh -# -# Environment Variables (optional): -# AWS_REGION - AWS region (default: us-east-1) -# AWS_ACCOUNT_ID - AWS account ID (required, or set below) -# REPOSITORY_NAME - ECR repository name (default: mcp-gateway) -# -# ═══════════════════════════════════════════════════════════════════════════════ - -# ────────────────────────────────────────────────────────────────────────────── -# Configuration -# ────────────────────────────────────────────────────────────────────────────── -AWS_REGION="${AWS_REGION:-us-east-1}" -AWS_ACCOUNT_ID="${AWS_ACCOUNT_ID:-745308818994}" # Set your AWS Account ID -REPOSITORY_NAME="${REPOSITORY_NAME:-mcp-gateway}" - -# Tags: timestamped version + stable + arch-specific -VERSION="$(date +%Y.%m.%d-%H%M)" -IMAGE_TAGS=("latest" "amd64" "arm64" "$VERSION") - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY_NAME}" - -# ────────────────────────────────────────────────────────────────────────────── -# Script directory handling -# ────────────────────────────────────────────────────────────────────────────── -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" -echo -e "${BLUE} MCP Gateway - Docker Build & Push${NC}" -echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" -echo -echo -e "${YELLOW}Repository:${NC} ${ECR_REPO}" -echo -e "${YELLOW}Version:${NC} ${VERSION}" -echo -e "${YELLOW}Project:${NC} ${PROJECT_ROOT}" -echo - -# ────────────────────────────────────────────────────────────────────────────── -# Preflight checks -# ────────────────────────────────────────────────────────────────────────────── -echo -e "${YELLOW}Running preflight checks...${NC}" - -# Check AWS CLI -if ! command -v aws >/dev/null 2>&1; then - echo -e "${RED}Error: AWS CLI not found${NC}" - echo "Install with: brew install awscli" - exit 1 -fi - -# Check Docker -if ! docker info >/dev/null 2>&1; then - echo -e "${RED}Error: Docker is not running${NC}" - exit 1 -fi - -echo -e "${GREEN}✓ AWS CLI found${NC}" -echo -e "${GREEN}✓ Docker is running${NC}" -echo - -# ────────────────────────────────────────────────────────────────────────────── -# ECR Setup -# ────────────────────────────────────────────────────────────────────────────── -echo -e "${YELLOW}Setting up ECR...${NC}" - -# Ensure repository exists (idempotent) -if ! aws ecr describe-repositories \ - --repository-names "${REPOSITORY_NAME}" \ - --region "${AWS_REGION}" >/dev/null 2>&1; then - echo -e "${YELLOW}Creating ECR repository: ${REPOSITORY_NAME}${NC}" - aws ecr create-repository \ - --repository-name "${REPOSITORY_NAME}" \ - --region "${AWS_REGION}" >/dev/null -fi - -# Login to ECR -echo -e "${YELLOW}Logging in to ECR...${NC}" -aws ecr get-login-password --region "${AWS_REGION}" \ - | docker login --username AWS --password-stdin \ - "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" - -echo -e "${GREEN}✓ ECR setup complete${NC}" -echo - -# ────────────────────────────────────────────────────────────────────────────── -# Docker Buildx Setup -# ────────────────────────────────────────────────────────────────────────────── -echo -e "${YELLOW}Setting up Docker Buildx...${NC}" - -BUILDER_NAME="mcp-gateway-builder" - -# Create/select builder if needed -if ! docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then - echo -e "${YELLOW}Creating buildx builder: ${BUILDER_NAME}${NC}" - docker buildx create --name "${BUILDER_NAME}" --use >/dev/null -else - docker buildx use "${BUILDER_NAME}" >/dev/null -fi - -echo -e "${GREEN}✓ Buildx builder ready${NC}" -echo - -# ────────────────────────────────────────────────────────────────────────────── -# Build and Push -# ────────────────────────────────────────────────────────────────────────────── -echo -e "${YELLOW}Building multi-arch image (linux/amd64, linux/arm64)...${NC}" -echo -e "${YELLOW}This may take several minutes...${NC}" -echo - -# Construct -t flags for all tags -TAG_FLAGS=() -for t in "${IMAGE_TAGS[@]}"; do - TAG_FLAGS+=(-t "${ECR_REPO}:${t}") -done - -# Change to project root for Docker build context -cd "${PROJECT_ROOT}" - -# Build and push -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - "${TAG_FLAGS[@]}" \ - -f docker/Dockerfile \ - --push \ - . - -echo -echo -e "${GREEN}✓ Build and push complete${NC}" -echo - -# ────────────────────────────────────────────────────────────────────────────── -# Verify Manifest -# ────────────────────────────────────────────────────────────────────────────── -echo -e "${YELLOW}Verifying pushed manifest...${NC}" - -if docker buildx imagetools inspect "${ECR_REPO}:latest" >/dev/null 2>&1; then - docker buildx imagetools inspect "${ECR_REPO}:latest" -else - echo -e "${YELLOW}Note: docker buildx imagetools not available; skipping manifest inspection${NC}" -fi - -# ────────────────────────────────────────────────────────────────────────────── -# Summary -# ────────────────────────────────────────────────────────────────────────────── -echo -echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" -echo -e "${GREEN}✅ Push complete!${NC}" -echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" -echo -echo -e "${GREEN}Images pushed:${NC}" -for t in "${IMAGE_TAGS[@]}"; do - echo -e " - ${ECR_REPO}:${t}" -done -echo -echo -e "${YELLOW}Kubernetes deployment:${NC}" -echo " kubectl set image deploy/ mcp-gateway=${ECR_REPO}:${VERSION}" -echo -echo -e "${YELLOW}Or use latest tag and restart pods:${NC}" -echo " kubectl delete pod -l app=mcp-gateway" -echo -echo -e "${YELLOW}Test locally:${NC}" -echo " docker run -p 3003:3003 \\" -echo " -e MCP_GATEWAY_CONFIG='{\"version\":\"2026-01-11\",\"metadata\":{\"gatewayId\":\"123\"},\"config\":{},\"servers\":[{\"serverId\":\"1\",\"name\":\"test\",\"url\":\"http://host.docker.internal:3001/mcp\"}]}' \\" -echo " ${ECR_REPO}:latest" -echo diff --git a/third_party/gopher-orch/docs/Agent.md b/third_party/gopher-orch/docs/Agent.md deleted file mode 100644 index 6e85e1f1..00000000 --- a/third_party/gopher-orch/docs/Agent.md +++ /dev/null @@ -1,497 +0,0 @@ -# Agent Design Document - -## Overview - -The Agent module implements the ReAct (Reasoning + Acting) pattern for building AI agents that can use tools to accomplish tasks. The agent iteratively calls an LLM, executes requested tools, and feeds results back until the task is complete. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ ReActAgent │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ AgentConfig │ │ -│ │ • system_prompt • max_iterations • timeout │ │ -│ │ • llm_config • parallel_tool_calls │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ LLMProvider │ │ ToolExecutor │ │ AgentState │ │ -│ │ │ │ │ │ │ │ -│ │ • chat() │ │ • executeTool() │ │ • messages │ │ -│ │ • toolCalls │ │ • registry() │ │ • steps │ │ -│ └─────────────────┘ └─────────────────┘ │ • status │ │ -│ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## ReAct Loop Flow - -``` - ┌─────────────┐ - │ Start │ - └──────┬──────┘ - │ - ▼ - ┌───────────────────────┐ - │ Add user query to │ - │ message history │ - └───────────┬───────────┘ - │ - ┌────────────────┼────────────────┐ - │ ▼ │ - │ ┌───────────────────────┐ │ - │ │ Check iteration & │ │ - │ │ timeout limits │ │ - │ └───────────┬───────────┘ │ - │ │ │ - │ ┌──────┴──────┐ │ - │ │ Exceeded? │ │ - │ └──────┬──────┘ │ - │ Yes/ │ \No │ - │ / │ \ │ - │ ▼ │ ▼ │ - │ ┌─────────┐ │ ┌─────────────────┐ - │ │ FAIL │ │ │ Call LLM │ - │ └─────────┘ │ │ with tools │ - │ │ └────────┬────────┘ - │ │ │ - │ │ ▼ - │ │ ┌─────────────────┐ - │ │ │ Record step │ - │ │ └────────┬────────┘ - │ │ │ - │ │ ▼ - │ │ ┌─────────────────┐ - │ │ │ Has tool calls? │ - │ │ └────────┬────────┘ - │ │ Yes/ │ \No - │ │ / │ \ - │ │ ▼ │ ▼ - │ │ ┌──────────┐│ ┌──────────┐ - │ │ │ Execute ││ │ COMPLETE │ - │ │ │ tools ││ └──────────┘ - │ │ └────┬─────┘│ - │ │ │ │ - │ │ ▼ │ - │ │ ┌──────────┐│ - │ │ │Add tool ││ - │ │ │results to││ - │ │ │messages ││ - │ │ └────┬─────┘│ - │ │ │ │ - └─────────────────┼──────┘ │ - │ │ - └─────────────┘ - (loop) -``` - -## Core Components - -### 1. AgentConfig - -```cpp -struct AgentConfig { - LLMConfig llm_config; // Model settings - std::string system_prompt; // Agent behavior definition - int max_iterations = 10; // Prevent infinite loops - optional max_total_tokens; // Token budget - std::chrono::milliseconds timeout{300000}; // 5 min default - bool parallel_tool_calls = true; - - // Builder pattern - AgentConfig& withModel(const std::string& model); - AgentConfig& withSystemPrompt(const std::string& prompt); - AgentConfig& withMaxIterations(int iterations); - AgentConfig& withTemperature(double t); -}; -``` - -### 2. AgentState - -```cpp -enum class AgentStatus { - IDLE, // Not started - RUNNING, // Currently executing - COMPLETED, // Finished successfully - FAILED, // Error occurred - CANCELLED, // Cancelled by user - MAX_ITERATIONS_REACHED // Hit iteration limit -}; - -struct AgentState { - AgentStatus status; - std::vector messages; // Conversation history - std::vector steps; // Execution steps - int current_iteration; - Usage total_usage; // Token counts - optional error; -}; -``` - -### 3. AgentStep - -```cpp -struct ToolExecution { - std::string tool_name; - std::string call_id; - JsonValue input; - JsonValue output; - bool success; - std::string error_message; -}; - -struct AgentStep { - int step_number; - Message llm_message; - optional llm_usage; - std::vector tool_executions; - std::chrono::milliseconds llm_duration; -}; -``` - -### 4. Callbacks - -```cpp -// Called when agent completes -using AgentCallback = std::function)>; - -// Called after each step (for progress monitoring) -using StepCallback = std::function; - -// Called before tool execution (can approve/reject) -using ToolApprovalCallback = std::function; -``` - -## Detailed Execution Flow - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ ReActAgent::run() │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 1. Initialize State │ -│ • status = RUNNING │ -│ • Add context messages (if any) │ -│ • Add user query as USER message │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 2. executeLoop() │ -│ • Check cancellation flag │ -│ • Check iteration limit (current_iteration >= max_iterations) │ -│ • Check timeout (elapsed > config.timeout) │ -│ • Increment current_iteration │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 3. callLLM() │ -│ • Build messages from state │ -│ • Get tool specs from registry │ -│ • Call provider->chat(messages, tools, config, dispatcher, callback) │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 4. On LLM Response │ -│ • Create AgentStep with LLM message and usage │ -│ • Record step (triggers step callback) │ -│ • Call handleLLMResponse() │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ┌────────────┴────────────┐ - │ │ - Has Tool Calls? No Tool Calls - │ │ - ▼ ▼ -┌─────────────────────────────────┐ ┌─────────────────────────────────┐ -│ 5a. executeToolCalls() │ │ 5b. completeRun(COMPLETED) │ -│ • Check approval callback │ │ • Set status │ -│ • Call executor.executeTool │ │ • Build AgentResult │ -│ for each tool │ │ • Invoke completion callback│ -│ • Collect results │ └─────────────────────────────────┘ -└─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 6. handleToolResults() │ -│ • Update last step with tool executions │ -│ • Add TOOL messages for each result │ -│ • Post to dispatcher: executeLoop() (continue loop) │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -## Example Usage - -### Basic Agent - -```cpp -#include "gopher/orch/agent/agent.h" -#include "gopher/orch/llm/openai_provider.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; - -// Create provider -auto provider = createOpenAIProvider("sk-your-api-key"); - -// Create tool registry -auto registry = makeToolRegistry(); - -// Add a simple tool -JsonValue searchSchema = JsonValue::object(); -searchSchema["type"] = "object"; -JsonValue props = JsonValue::object(); -JsonValue queryProp = JsonValue::object(); -queryProp["type"] = "string"; -props["query"] = queryProp; -searchSchema["properties"] = props; - -registry->addSyncTool("search", "Search the web", searchSchema, - [](const JsonValue& args) -> Result { - std::string query = args["query"].getString(); - // Perform search... - JsonValue result = JsonValue::object(); - result["results"] = "Search results for: " + query; - return Result(result); - }); - -// Configure agent -AgentConfig config("gpt-4"); -config.withSystemPrompt("You are a helpful assistant with web search capability.") - .withMaxIterations(5) - .withTemperature(0.7); - -// Create agent -auto agent = ReActAgent::create(provider, registry, config); - -// Run agent -agent->run("What is the weather like in Tokyo today?", dispatcher, - [](Result result) { - if (mcp::holds_alternative(result)) { - auto& agentResult = mcp::get(result); - std::cout << "Response: " << agentResult.response << std::endl; - std::cout << "Steps: " << agentResult.iterationCount() << std::endl; - std::cout << "Tokens: " << agentResult.total_usage.total_tokens << std::endl; - } else { - auto& error = mcp::get(result); - std::cerr << "Agent failed: " << error.message << std::endl; - } - }); -``` - -### Agent with Progress Monitoring - -```cpp -auto agent = ReActAgent::create(provider, registry, config); - -// Monitor each step -agent->setStepCallback([](const AgentStep& step) { - std::cout << "Step " << step.step_number << ":" << std::endl; - std::cout << " LLM response: " << step.llm_message.content << std::endl; - - if (!step.tool_executions.empty()) { - std::cout << " Tool executions:" << std::endl; - for (const auto& exec : step.tool_executions) { - std::cout << " - " << exec.tool_name - << (exec.success ? " (success)" : " (failed)") - << std::endl; - } - } -}); - -agent->run("Research the latest AI developments", dispatcher, callback); -``` - -### Agent with Tool Approval - -```cpp -auto agent = ReActAgent::create(provider, registry, config); - -// Require approval for dangerous tools -agent->setToolApprovalCallback([](const ToolCall& call) -> bool { - if (call.name == "delete_file" || call.name == "execute_command") { - std::cout << "Tool '" << call.name << "' requires approval." << std::endl; - std::cout << "Arguments: " << call.arguments.toString() << std::endl; - std::cout << "Approve? (y/n): "; - - std::string input; - std::getline(std::cin, input); - return input == "y" || input == "yes"; - } - return true; // Auto-approve other tools -}); - -agent->run("Clean up temp files", dispatcher, callback); -``` - -### Agent with Context - -```cpp -// Provide conversation history -std::vector context = { - Message::user("My name is Alice and I work at Acme Corp."), - Message::assistant("Hello Alice! Nice to meet you. How can I help you today?") -}; - -agent->run("What company do I work at?", context, dispatcher, - [](Result result) { - // Agent can access previous context - // Response: "You work at Acme Corp." - }); -``` - -### Multiple Tools Agent - -```cpp -auto registry = makeToolRegistry(); - -// Calculator tool -registry->addSyncTool("calculate", "Perform math calculations", calcSchema, - [](const JsonValue& args) -> Result { - std::string expr = args["expression"].getString(); - // Evaluate expression... - return Result(JsonValue(42.0)); - }); - -// Weather tool -registry->addSyncTool("get_weather", "Get current weather", weatherSchema, - [](const JsonValue& args) -> Result { - std::string city = args["city"].getString(); - JsonValue result = JsonValue::object(); - result["temperature"] = 72; - result["condition"] = "sunny"; - return Result(result); - }); - -// Time tool -registry->addSyncTool("get_time", "Get current time", timeSchema, - [](const JsonValue& args) -> Result { - JsonValue result = JsonValue::object(); - result["time"] = "2:30 PM"; - result["timezone"] = "PST"; - return Result(result); - }); - -// Agent can now use all three tools -agent->run( - "What's the weather in Seattle, what time is it there, and what is 15 * 7?", - dispatcher, callback); -``` - -### Cancellation - -```cpp -auto agent = ReActAgent::create(provider, registry, config); - -// Start long-running task -agent->run("Analyze this large dataset...", dispatcher, callback); - -// Cancel from another thread or timer -std::this_thread::sleep_for(std::chrono::seconds(30)); -if (agent->isRunning()) { - agent->cancel(); - // Callback will receive CANCELLED status -} -``` - -## Message Flow Example - -``` -User: "What's 25 * 4 and what's the weather in Paris?" - -┌───────────────────────────────────────────────────────────────────────────┐ -│ Iteration 1 │ -├───────────────────────────────────────────────────────────────────────────┤ -│ Messages to LLM: │ -│ [SYSTEM] You are a helpful assistant with tools. │ -│ [USER] What's 25 * 4 and what's the weather in Paris? │ -│ │ -│ LLM Response: │ -│ [ASSISTANT] I'll help you with both. Let me calculate and check weather. │ -│ Tool calls: │ -│ 1. calculate({expression: "25 * 4"}) │ -│ 2. get_weather({city: "Paris"}) │ -└───────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌───────────────────────────────────────────────────────────────────────────┐ -│ Tool Execution │ -├───────────────────────────────────────────────────────────────────────────┤ -│ calculate({expression: "25 * 4"}) → {result: 100} │ -│ get_weather({city: "Paris"}) → {temp: 18, condition: "cloudy"} │ -└───────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌───────────────────────────────────────────────────────────────────────────┐ -│ Iteration 2 │ -├───────────────────────────────────────────────────────────────────────────┤ -│ Messages to LLM: │ -│ [SYSTEM] You are a helpful assistant with tools. │ -│ [USER] What's 25 * 4 and what's the weather in Paris? │ -│ [ASSISTANT] I'll help you with both... │ -│ [TOOL] call_1: {result: 100} │ -│ [TOOL] call_2: {temp: 18, condition: "cloudy"} │ -│ │ -│ LLM Response: │ -│ [ASSISTANT] 25 × 4 = 100, and Paris is currently 18°C and cloudy. │ -│ (No tool calls - conversation complete) │ -└───────────────────────────────────────────────────────────────────────────┘ - │ - ▼ - Agent COMPLETED - Response: "25 × 4 = 100, and Paris - is currently 18°C and cloudy." -``` - -## Error Handling - -```cpp -namespace AgentError { - enum : int { - OK = 0, - NO_PROVIDER = -200, // No LLM provider configured - NO_TOOLS = -201, // No tools available - MAX_ITERATIONS = -202, // Hit iteration limit - TIMEOUT = -203, // Timeout exceeded - TOOL_EXECUTION_FAILED = -204, - LLM_ERROR = -205, // LLM call failed - CANCELLED = -206, // User cancelled - UNKNOWN = -299 - }; -} - -// Handle different outcomes -agent->run(query, dispatcher, [](Result result) { - if (mcp::holds_alternative(result)) { - auto& r = mcp::get(result); - switch (r.status) { - case AgentStatus::COMPLETED: - // Success - break; - case AgentStatus::MAX_ITERATIONS_REACHED: - // Task too complex, consider breaking it down - break; - case AgentStatus::CANCELLED: - // User cancelled - break; - } - } else { - auto& error = mcp::get(result); - // Handle error based on code - } -}); -``` - -## Best Practices - -1. **Set appropriate limits**: Configure `max_iterations` and `timeout` based on task complexity -2. **Use clear system prompts**: Guide the agent's behavior and tool usage -3. **Handle tool errors gracefully**: Tools should return meaningful error messages -4. **Monitor with step callbacks**: Track progress for long-running tasks -5. **Implement approval for sensitive tools**: Use `ToolApprovalCallback` for destructive operations -6. **Provide relevant context**: Include conversation history when continuity matters diff --git a/third_party/gopher-orch/docs/AgentRunnable.md b/third_party/gopher-orch/docs/AgentRunnable.md deleted file mode 100644 index f82e006d..00000000 --- a/third_party/gopher-orch/docs/AgentRunnable.md +++ /dev/null @@ -1,863 +0,0 @@ -# Agent-Runnable Integration Design - -## Overview - -This document describes how `Agent`, `Runnable`, and `LLM` components work together in gopher-orch, enabling seamless composition of AI agents with other workflow components. - -The design is inspired by LangChain, LangGraph, and n8n patterns, adapted for C++ with async-first, dispatcher-based execution. - -## Goals - -1. **Composability**: Agents can be used anywhere a `Runnable` is expected -2. **Consistency**: Same patterns for LLM, Tools, and Agents -3. **Flexibility**: Support both direct Agent usage and Runnable composition -4. **Type Safety**: Leverage C++ templates while maintaining JSON interoperability - -## Architecture - -### Three-Level Runnable Hierarchy - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ RUNNABLE LAYER │ -│ │ -│ Level 3: Graph Runnables (Complex Workflows) │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ CompiledStateGraph │ │ -│ │ (Nodes + Edges + State with Reducers) │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ Level 2: Composite Runnables (Composition Patterns) │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ -│ │ Sequence │ │ Parallel │ │ Router │ │ AgentRunnable │ │ -│ │ (A→B→C) │ │ (A|B|C) │ │ (if/else) │ │ (LLM↔Tools) │ │ -│ └────────────┘ └────────────┘ └────────────┘ └────────────────────┘ │ -│ │ │ -│ Level 1: Primitive Runnables (Leaf Nodes) │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ -│ │ Lambda │ │LLMRunnable │ │ToolRunnable│ │ Other Leaves │ │ -│ │ (function) │ │ (LLM API) │ │(tool exec) │ │ │ │ -│ └────────────┘ └────────────┘ └────────────┘ └────────────────────┘ │ -│ │ -│ Foundation: Runnable │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ invoke(input, config, dispatcher, callback) │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Component Relationships - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ LLMProvider │ │ToolRegistry │ │ ToolExecutor │ │ -│ │ (API calls) │ │ (storage) │ │ (execution) │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ -│ ▼ └────────┬───────────────┘ │ -│ ┌──────────────┐ │ │ -│ │ LLMRunnable │ ▼ │ -│ │ (wrapper) │ ┌──────────────┐ │ -│ └──────┬───────┘ │ ToolRunnable │ │ -│ │ │ (wrapper) │ │ -│ │ └──────┬───────┘ │ -│ │ │ │ -│ └─────────────┬───────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────┐ │ -│ │ AgentRunnable │ │ -│ │ │ │ -│ │ ┌───────────────┐ │ │ -│ │ │ Agent Graph │ │ │ -│ │ │ (LLM↔Tools) │ │ │ -│ │ └───────────────┘ │ │ -│ └──────────┬──────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────┐ │ -│ │ Runnable│ │ -│ │ (composable) │ │ -│ └─────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -## Core Components - -### 1. LLMRunnable - -Wraps `LLMProvider` as a `Runnable`. - -**Purpose**: Makes LLM calls composable with other Runnables. - -**Header**: `include/gopher/orch/llm/llm_runnable.h` - -```cpp -class LLMRunnable : public Runnable { - public: - explicit LLMRunnable(LLMProviderPtr provider, - const LLMConfig& config = LLMConfig()); - - std::string name() const override; - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override; - - private: - LLMProviderPtr provider_; - LLMConfig default_config_; -}; -``` - -**Input Schema**: -```json -{ - "messages": [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello!"} - ], - "tools": [ - {"name": "search", "description": "...", "parameters": {...}} - ], - "config": { - "temperature": 0.7, - "max_tokens": 1000 - } -} -``` - -**Output Schema**: -```json -{ - "message": { - "role": "assistant", - "content": "Hi there!", - "tool_calls": [ - {"id": "call_1", "name": "search", "arguments": {"query": "..."}} - ] - }, - "finish_reason": "tool_calls", - "usage": { - "prompt_tokens": 50, - "completion_tokens": 20, - "total_tokens": 70 - } -} -``` - -### 2. ToolRunnable - -Wraps `ToolExecutor` as a `Runnable`. - -**Purpose**: Makes tool execution composable, supports parallel tool calls. - -**Header**: `include/gopher/orch/agent/tool_runnable.h` - -```cpp -class ToolRunnable : public Runnable { - public: - explicit ToolRunnable(ToolExecutorPtr executor); - - std::string name() const override; - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override; - - private: - ToolExecutorPtr executor_; -}; -``` - -**Input Schema** (single tool call): -```json -{ - "id": "call_123", - "name": "search", - "arguments": {"query": "weather in Tokyo"} -} -``` - -**Input Schema** (multiple tool calls - parallel execution): -```json -{ - "tool_calls": [ - {"id": "call_1", "name": "search", "arguments": {"query": "weather"}}, - {"id": "call_2", "name": "calculator", "arguments": {"expr": "2+2"}} - ] -} -``` - -**Output Schema**: -```json -{ - "results": [ - {"id": "call_1", "result": {"temperature": 25}, "success": true}, - {"id": "call_2", "result": 4, "success": true} - ] -} -``` - -### 3. AgentState - -State container that flows through the agent graph, with reducer support. - -**Header**: `include/gopher/orch/agent/agent_state.h` - -```cpp -struct AgentState { - std::vector messages; // Conversation history - int remaining_steps = 10; // Iteration counter - optional error; // Error state - - // Reducer: merge state updates (messages are APPENDED) - static AgentState reduce(const AgentState& current, - const AgentState& update); - - // Serialize to/from JSON for graph nodes - JsonValue toJson() const; - static AgentState fromJson(const JsonValue& json); -}; -``` - -**Reducer Semantics**: -```cpp -// Messages use APPEND reducer (like LangGraph's add_messages) -AgentState AgentState::reduce(const AgentState& current, - const AgentState& update) { - AgentState result; - - // Append new messages to existing - result.messages = current.messages; - for (const auto& msg : update.messages) { - result.messages.push_back(msg); - } - - // Other fields use last-write-wins - result.remaining_steps = update.remaining_steps; - result.error = update.error; - - return result; -} -``` - -### 4. AgentRunnable - -The main integration point - wraps Agent functionality as a composable Runnable. - -**Header**: `include/gopher/orch/agent/agent_runnable.h` - -```cpp -class AgentRunnable : public Runnable { - public: - using Ptr = std::shared_ptr; - - // Factory methods - static Ptr create(LLMProviderPtr provider, - ToolExecutorPtr tools, - const AgentConfig& config = AgentConfig()); - - static Ptr create(LLMProviderPtr provider, - ToolRegistryPtr registry, - const AgentConfig& config = AgentConfig()); - - std::string name() const override; - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override; - - // Accessors - void setStepCallback(StepCallback callback); - void setToolApprovalCallback(ToolApprovalCallback callback); - - private: - // Internal graph nodes - std::shared_ptr llm_node_; - std::shared_ptr tool_node_; - AgentConfig config_; - - // Graph execution - void runLoop(AgentState& state, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback); - - std::string shouldContinue(const AgentState& state); -}; -``` - -**Input Schema**: -```json -{ - "query": "What is the weather in Tokyo?", - "context": [ - {"role": "user", "content": "Previous message"} - ], - "config": { - "max_iterations": 5 - } -} -``` - -Alternative input formats (auto-detected): -```json -// String input -"What is the weather?" - -// LangGraph-style messages input -{ - "messages": [ - {"role": "user", "content": "What is the weather?"} - ] -} -``` - -**Output Schema**: -```json -{ - "response": "The weather in Tokyo is 25°C and sunny.", - "status": "completed", - "iterations": 2, - "messages": [...], - "usage": { - "prompt_tokens": 150, - "completion_tokens": 50, - "total_tokens": 200 - }, - "duration_ms": 3500 -} -``` - -## Agent Internal Graph Structure - -AgentRunnable internally operates as a graph, following the LangGraph pattern: - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ AGENT INTERNAL GRAPH │ -└─────────────────────────────────────────────────────────────────────────────┘ - - INPUT - │ - ▼ - ┌─────────────────────┐ - │ Parse Input │ - │ (extract query, │ - │ context, config) │ - └──────────┬──────────┘ - │ - ▼ - ┌─────────────────────┐ - │ Initialize State │ - │ AgentState { │ - │ messages: [...], │ - │ remaining: 10 │ - │ } │ - └──────────┬──────────┘ - │ - ┌──────────────────────┴──────────────────────┐ - │ │ - │ LOOP │ - │ │ - │ ┌─────────────────────────────────┐ │ - │ │ LLM Node │ │ - │ │ (LLMRunnable) │ │ - │ │ │ │ - │ │ Input: state.messages │ │ - │ │ Output: assistant message │ │ - │ └────────────────┬────────────────┘ │ - │ │ │ - │ ▼ │ - │ ┌─────────────────────────────────┐ │ - │ │ should_continue() │ │ - │ │ │ │ - │ │ - has_tool_calls? → "tools" │ │ - │ │ - no_tool_calls? → "end" │ │ - │ │ - max_iterations? → "end" │ │ - │ └────────────────┬────────────────┘ │ - │ │ │ - │ ┌─────────┴─────────┐ │ - │ │ │ │ - │ ▼ ▼ │ - │ ┌─────────────┐ ┌───────────┐ │ - │ │ Tools Node │ │ END │────┼───► OUTPUT - │ │(ToolRunnable│ └───────────┘ │ - │ │ parallel) │ │ - │ └──────┬──────┘ │ - │ │ │ - │ │ (append tool results │ - │ │ to state.messages) │ - │ │ │ - │ └─────────────────────────────┘ - │ │ - └──────────────────────┘ -``` - -## Usage Examples - -### Example 1: Direct AgentRunnable Usage - -```cpp -#include "gopher/orch/agent/agent_runnable.h" - -// Create components -auto provider = createOpenAIProvider("sk-..."); -auto registry = makeToolRegistry(); -registry->addTool("search", "Search the web", schema, searchHandler); - -// Create agent runnable -auto agent = AgentRunnable::create(provider, registry, - AgentConfig("gpt-4o").withMaxIterations(5)); - -// Invoke as Runnable -JsonValue input = JsonValue::object(); -input["query"] = "What is the weather in Tokyo?"; - -agent->invoke(input, RunnableConfig(), dispatcher, - [](Result result) { - if (isSuccess(result)) { - std::cout << getValue(result)["response"].getString() << std::endl; - } - }); -``` - -### Example 2: Agent in Sequence Pipeline - -```cpp -#include "gopher/orch/composition/sequence.h" -#include "gopher/orch/agent/agent_runnable.h" - -// Preprocessing: extract and validate query -auto preprocess = makeJsonLambda([](const JsonValue& input) { - JsonValue output = JsonValue::object(); - output["query"] = sanitize(input["user_input"].getString()); - return makeSuccess(output); -}, "Preprocess"); - -// Postprocessing: format response -auto postprocess = makeJsonLambda([](const JsonValue& input) { - JsonValue output = JsonValue::object(); - output["answer"] = input["response"]; - output["source"] = "AI Assistant"; - return makeSuccess(output); -}, "Postprocess"); - -// Build pipeline -auto pipeline = sequence("AgentPipeline") - .add(preprocess) - .add(AgentRunnable::create(provider, registry)) - .add(postprocess) - .build(); - -// Execute -pipeline->invoke(userInput, config, dispatcher, callback); -``` - -### Example 3: Multi-Agent Router - -```cpp -#include "gopher/orch/composition/router.h" -#include "gopher/orch/agent/agent_runnable.h" - -// Different agents for different tasks -auto codeAgent = AgentRunnable::create(codeProvider, codeTools, - AgentConfig("gpt-4o").withSystemPrompt("You are a coding assistant.")); - -auto researchAgent = AgentRunnable::create(researchProvider, searchTools, - AgentConfig("gpt-4o").withSystemPrompt("You are a research assistant.")); - -auto generalAgent = AgentRunnable::create(provider, {}, - AgentConfig("gpt-4o")); - -// Route based on query type -auto agentRouter = router("AgentRouter") - .when([](const JsonValue& in) { - return in["query"].getString().find("code") != std::string::npos; - }, codeAgent) - .when([](const JsonValue& in) { - return in["query"].getString().find("search") != std::string::npos; - }, researchAgent) - .otherwise(generalAgent) - .build(); - -agentRouter->invoke(input, config, dispatcher, callback); -``` - -### Example 4: Agent in StateGraph Workflow - -```cpp -#include "gopher/orch/graph/state_graph.h" -#include "gopher/orch/agent/agent_runnable.h" - -// Build complex workflow -StateGraph workflow; - -// Add nodes -workflow.addNode("classifier", makeJsonLambda([](const JsonValue& in) { - // Classify the request - JsonValue out = in; - out["category"] = classify(in["query"].getString()); - return makeSuccess(out); -}, "Classifier")); - -workflow.addNode("agent", AgentRunnable::create(provider, tools)); - -workflow.addNode("validator", makeJsonLambda([](const JsonValue& in) { - // Validate agent response - JsonValue out = in; - out["valid"] = validate(in["response"].getString()); - return makeSuccess(out); -}, "Validator")); - -// Add edges -workflow.setEntryPoint("classifier"); -workflow.addConditionalEdge("classifier", [](const GraphState& s) { - return s.get("category").getString() == "complex" ? "agent" : "end"; -}); -workflow.addEdge("agent", "validator"); -workflow.addConditionalEdge("validator", [](const GraphState& s) { - return s.get("valid").getBool() ? "end" : "agent"; // Retry if invalid -}); - -// Compile and run -auto compiled = workflow.compile(); -compiled->invoke(input, config, dispatcher, callback); -``` - -### Example 5: Parallel Multi-Agent - -```cpp -#include "gopher/orch/composition/parallel.h" -#include "gopher/orch/agent/agent_runnable.h" - -// Run multiple specialized agents in parallel -auto multiAgent = parallel("MultiAgentResearch") - .add("web_search", AgentRunnable::create(provider, webSearchTools)) - .add("academic", AgentRunnable::create(provider, academicTools)) - .add("news", AgentRunnable::create(provider, newsTools)) - .build(); - -// Result combines all agent outputs -// {"web_search": {...}, "academic": {...}, "news": {...}} -multiAgent->invoke(input, config, dispatcher, callback); -``` - -### Example 6: Agent with Resilience - -```cpp -#include "gopher/orch/resilience/retry.h" -#include "gopher/orch/resilience/timeout.h" -#include "gopher/orch/agent/agent_runnable.h" - -auto agent = AgentRunnable::create(provider, tools); - -// Add timeout per invocation -auto timedAgent = Timeout::create( - agent, - std::chrono::seconds(60) -); - -// Add retry with exponential backoff -auto resilientAgent = Retry::create( - timedAgent, - RetryPolicy::exponential(3, 1000) // 3 attempts, 1s initial delay -); - -resilientAgent->invoke(input, config, dispatcher, callback); -``` - -## File Structure - -``` -include/gopher/orch/ -├── core/ -│ ├── runnable.h # Base Runnable template -│ ├── lambda.h # Lambda wrapper -│ ├── config.h # RunnableConfig -│ └── types.h # Core types (Result, Error, etc.) -│ -├── llm/ -│ ├── llm_provider.h # LLMProvider interface -│ ├── llm_types.h # Message, ToolCall, LLMResponse -│ ├── llm_runnable.h # NEW: LLMRunnable wrapper -│ ├── openai_provider.h # OpenAI implementation -│ └── anthropic_provider.h # Anthropic implementation -│ -├── agent/ -│ ├── agent.h # Agent interface (direct use) -│ ├── agent_types.h # AgentConfig, AgentResult -│ ├── agent_state.h # NEW: AgentState with reducers -│ ├── agent_runnable.h # NEW: AgentRunnable (composable) -│ ├── tool_registry.h # Tool storage -│ ├── tool_executor.h # Tool execution -│ ├── tool_runnable.h # NEW: ToolRunnable wrapper -│ └── tool_definition.h # Tool types -│ -├── composition/ -│ ├── sequence.h # Sequential composition -│ ├── parallel.h # Parallel composition -│ └── router.h # Conditional routing -│ -├── resilience/ -│ ├── retry.h # Retry wrapper -│ ├── timeout.h # Timeout wrapper -│ ├── circuit_breaker.h # Circuit breaker -│ └── fallback.h # Fallback wrapper -│ -└── graph/ - ├── state_graph.h # StateGraph builder - ├── graph_state.h # GraphState container - ├── graph_node.h # Node types - └── compiled_graph.h # CompiledStateGraph -``` - -## Design Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Wrapper vs Inheritance | Wrapper (Option A) | C++ single inheritance, type safety, flexibility | -| State Management | AgentState with reducers | Enables parallel tools, clear message history | -| Input/Output Types | JsonValue | Flexible, interoperable with all components | -| Internal Structure | Graph-based | Matches LangGraph, enables complex flows | -| Tool Execution | Parallel by default | Performance, matches LLM batch tool calls | -| Error Handling | Result monad | Consistent with codebase, explicit errors | -| Tool Execution Location | Internal (Option 1) | Simpler execution flow, no context switching | -| Connection Types | Optional enhancement | Useful for visual builders, not required initially | - -## Learnings from n8n - -n8n is a workflow automation platform with strong AI agent integration. Their architecture provides several patterns worth considering. - -### 1. Typed Connection System - -n8n uses `NodeConnectionTypes` to distinguish different connection semantics: - -```typescript -NodeConnectionTypes = { - AiAgent: 'ai_agent', - AiLanguageModel: 'ai_languageModel', - AiMemory: 'ai_memory', - AiTool: 'ai_tool', - AiOutputParser: 'ai_outputParser', - Main: 'main', // regular data flow -} -``` - -This allows nodes to have multiple typed input/output ports. An Agent node can accept: -- `AiLanguageModel` → the LLM connection -- `AiTool` → zero or more tool connections -- `AiMemory` → optional memory connection -- `Main` → trigger/data input - -**Applicable to gopher-orch**: We could add connection type hints for visual graph builders: - -```cpp -enum class ConnectionType { - Main, // Regular data flow - Tool, // Tool connection - Memory, // Memory/state connection - LLM // LLM provider connection -}; - -// Optional: typed edges in CompiledStateGraph -struct TypedEdge { - std::string from_node; - std::string to_node; - ConnectionType type; -}; -``` - -### 2. Engine Request/Response Pattern - -n8n separates tool calls into a request-response cycle: - -``` -Agent Node Engine - │ │ - ├── LLM returns tool calls ───►│ - │◄── EngineRequest (pause) ────┤ - │ │ - │ [Engine executes tool │ - │ nodes in parallel] │ - │ │ - │◄── EngineResponse (resume) ──┤ - ├── Continue with results ────►│ -``` - -**Key insight**: Tools execute *outside* the agent loop as independent nodes, enabling: -- **Tools as visual nodes** that can be connected in the UI -- **Parallel tool execution** at the engine level -- **Tool reusability** across different agents/workflows - -**Design options for gopher-orch**: - -| Option | Approach | Pros | Cons | -|--------|----------|------|------| -| Option 1 (Current) | Tools execute inside agent loop | Simpler, self-contained | Less visual, tools not reusable | -| Option 2 (n8n-style) | Agent yields tool requests | Visual composition, reusable tools | More complex, context switching | - -**Recommendation**: Start with Option 1 (internal execution). Add Option 2 later for visual builder use cases: - -```cpp -// Future: External tool execution mode -struct ToolRequest { - std::string tool_name; - JsonValue arguments; - std::string call_id; -}; - -// Agent can optionally yield pending tool calls -enum class AgentYieldReason { ToolCalls, Complete, Error }; - -struct AgentYield { - AgentYieldReason reason; - std::vector pending_tools; // If reason == ToolCalls - JsonValue result; // If reason == Complete -}; -``` - -### 3. RunnableSequence Composition - -n8n uses LangChain's `RunnableSequence.from([...])` for composing agent internals: - -```typescript -const runnableAgent = RunnableSequence.from([ - fallbackAgent ? agent.withFallbacks([fallbackAgent]) : agent, - getAgentStepsParser(outputParser, memory), - fixEmptyContentMessage, -]); -``` - -This validates our `Sequence<>` pattern for composing processing steps internally. - -### 4. Batching and Fallback - -n8n's `executeBatch` demonstrates: -- Batch processing multiple inputs through the same agent -- Built-in fallback model support -- `continueOnFail` error handling per item - -**Applicable to gopher-orch**: Consider adding to AgentConfig: - -```cpp -struct AgentConfig { - // ... existing fields ... - - // Fallback support (inspired by n8n) - LLMProviderPtr fallback_provider; - - // Batch processing - int batch_size = 1; - std::chrono::milliseconds delay_between_batches{0}; - bool continue_on_fail = false; -}; -``` - -### 5. Versioned Node Types - -n8n maintains backward compatibility via versioned implementations: - -```typescript -nodeVersions = { - 1: new AgentV1(baseDescription), - 2: new AgentV2(baseDescription), - 3: new AgentV3(baseDescription), -} -``` - -**Applicable to gopher-orch**: For production, consider versioning: - -```cpp -// Version in config -struct AgentConfig { - int version = 1; // For serialization compatibility - // ... -}; - -// Or version in class name for breaking changes -class AgentRunnableV2 : public Runnable { ... }; -``` - -### 6. DirectedGraph Operations - -n8n's `WorkflowExecute` uses `DirectedGraph.fromWorkflow(workflow)` for: -- Finding start nodes -- Detecting cycles (`handleCycles`) -- Partial execution (subgraph extraction) -- Dirty node tracking for re-execution - -**Applicable to gopher-orch**: Our `CompiledStateGraph` should support: - -```cpp -class CompiledStateGraph { - // Existing - void invoke(...); - - // Consider adding (inspired by n8n) - std::vector findStartNodes() const; - bool hasCycles() const; - CompiledStateGraph extractSubgraph( - const std::string& from, - const std::string& to) const; - - // Partial execution: re-run from a specific node - void invokePartial( - const std::string& start_node, - const GraphState& existing_state, - Dispatcher& dispatcher, - Callback callback); -}; -``` - -### Adoption Priority - -| Pattern | Priority | Recommendation | -|---------|----------|----------------| -| Typed connections | Low | Add later for visual builders | -| External tool execution | Low | Start internal, add external mode later | -| RunnableSequence composition | Already done | Validates our Sequence pattern | -| Fallback model support | Medium | Add to AgentConfig | -| Batch processing | Medium | Add to AgentConfig | -| Versioning | Medium | Add version field for compatibility | -| Graph operations | Medium | Add partial execution support | - -## Thread Safety - -All components follow the dispatcher-based threading model: - -1. **Invoke**: Called from dispatcher thread -2. **Callbacks**: Always invoked in dispatcher thread context -3. **State**: Not shared across threads; passed through callbacks -4. **Cancellation**: Atomic flag checked at safe points - -```cpp -// Thread safety contract -class AgentRunnable : public Runnable { - // invoke() must be called from dispatcher thread - // callback is always invoked in dispatcher thread - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, // All async work uses this - Callback callback) override; -}; -``` - -## References - -- LangChain Runnable: `langchain-core/runnables/base.py` -- LangGraph Pregel: `langgraph/pregel/main.py` -- LangGraph create_react_agent: `langgraph/prebuilt/chat_agent_executor.py` -- n8n Agent Node: `packages/@n8n/nodes-langchain/nodes/agents/Agent/` -- n8n ToolsAgent Execute: `nodes/agents/Agent/agents/ToolsAgent/V3/execute.ts` -- n8n NodeConnectionTypes: `packages/workflow/src/interfaces.ts:2169` -- n8n WorkflowExecute: `packages/core/src/execution-engine/workflow-execute.ts` -- gopher-orch Runnable: `include/gopher/orch/core/runnable.h` -- gopher-orch Agent: `include/gopher/orch/agent/agent.h` diff --git a/third_party/gopher-orch/docs/Composition.md b/third_party/gopher-orch/docs/Composition.md deleted file mode 100644 index 7a779aa8..00000000 --- a/third_party/gopher-orch/docs/Composition.md +++ /dev/null @@ -1,258 +0,0 @@ -# Composition Patterns - -Gopher Orch provides three core composition patterns for building complex workflows from simple components: **Sequence**, **Parallel**, and **Router**. - -## Overview - -| Pattern | Purpose | Behavior | -|---------|---------|----------| -| Sequence | Chain operations | Output of A becomes input of B | -| Parallel | Concurrent execution | Same input to all branches, collect results | -| Router | Conditional branching | Route to different handlers based on conditions | - -## Sequence - -Chain multiple runnables together where the output of one becomes the input of the next. - -### Basic Usage - -```cpp -#include "gopher/orch/composition/sequence.h" - -using namespace gopher::orch::composition; - -// Using pipe operator (type-safe) -auto pipeline = parseInput | processData | formatOutput; - -// Using builder (JSON runnables) -auto seq = sequence("MyPipeline") - .add(step1) - .add(step2) - .add(step3) - .build(); - -// Invoke -seq->invoke(input, config, dispatcher, callback); -``` - -### Type-Safe Chaining - -When types are known at compile time, use the `|` operator: - -```cpp -// Types must match: A's output = B's input -auto step1 = makeSyncLambda(...); // string -> int -auto step2 = makeSyncLambda(...); // int -> JsonValue - -auto pipeline = step1 | step2; // string -> JsonValue -``` - -### Dynamic Chaining - -For runtime-composed pipelines, use the builder: - -```cpp -auto builder = sequence("DynamicPipeline"); - -for (auto& step : steps) { - builder.add(step); -} - -auto pipeline = builder.build(); -``` - -### Error Handling - -Sequence **short-circuits on first error** - subsequent steps are not executed: - -```cpp -auto seq = sequence() - .add(mayFail) // If this fails... - .add(neverRuns) // ...this is skipped - .build(); -``` - -## Parallel - -Execute multiple runnables concurrently with the same input. - -### Basic Usage - -```cpp -#include "gopher/orch/composition/parallel.h" - -using namespace gopher::orch::composition; - -// Build parallel execution -auto par = parallel("FetchAll") - .add("weather", fetchWeather) - .add("news", fetchNews) - .add("stocks", fetchStocks) - .build(); - -// Invoke - all branches get the same input -par->invoke(input, config, dispatcher, [](Result result) { - // Result is an object with keys: weather, news, stocks - auto& data = mcp::get(result); - auto weather = data["weather"]; - auto news = data["news"]; - auto stocks = data["stocks"]; -}); -``` - -### Result Structure - -Results are collected into a JSON object with branch keys: - -```json -{ - "weather": { "temp": 72, "condition": "sunny" }, - "news": [ { "title": "..." }, ... ], - "stocks": { "AAPL": 150.00, ... } -} -``` - -### Fail-Fast Behavior - -By default, Parallel uses **fail-fast** semantics: -- First error cancels pending branches -- Error is returned immediately - -```cpp -auto par = parallel() - .add("fast", quickOp) // Completes first - .add("slow", slowOp) // If fast fails, slow is cancelled - .build(); -``` - -## Router - -Route input to different runnables based on conditions. - -### Basic Usage - -```cpp -#include "gopher/orch/composition/router.h" - -using namespace gopher::orch::composition; - -// JSON router with conditions -auto route = router("ActionRouter") - .when([](const JsonValue& input) { - return input["action"].getString() == "search"; - }, searchHandler) - .when([](const JsonValue& input) { - return input["action"].getString() == "calculate"; - }, calculateHandler) - .otherwise(defaultHandler) - .build(); - -// Invoke - routes to matching handler -route->invoke(input, config, dispatcher, callback); -``` - -### Type-Safe Router - -For typed runnables: - -```cpp -auto route = makeRouter("TypedRouter") - .when([](const std::string& s) { return s.starts_with("http"); }, httpHandler) - .when([](const std::string& s) { return s.starts_with("file"); }, fileHandler) - .otherwise(defaultHandler) - .build(); -``` - -### Condition Evaluation - -Conditions are evaluated in order: -1. First matching condition wins -2. If no match, uses `otherwise` handler -3. If no `otherwise`, returns error - -```cpp -auto route = router() - .when(isHighPriority, fastPath) // Checked first - .when(isNormalPriority, normalPath) // Checked second - .otherwise(slowPath) // Fallback - .build(); -``` - -## Combining Patterns - -Patterns can be nested and combined: - -```cpp -// Sequence with parallel step -auto pipeline = sequence() - .add(parseInput) - .add(parallel() - .add("validate", validator) - .add("enrich", enricher) - .build()) - .add(processResults) - .build(); - -// Router with sequence branches -auto workflow = router() - .when(isSimple, simpleHandler) - .when(isComplex, sequence() - .add(analyze) - .add(process) - .add(format) - .build()) - .otherwise(errorHandler) - .build(); -``` - -## With Resilience Patterns - -Add reliability to composed workflows: - -```cpp -#include "gopher/orch/resilience/retry.h" -#include "gopher/orch/resilience/timeout.h" - -// Parallel with timeout -auto bounded = withTimeout( - parallel() - .add("api1", fetchFromApi1) - .add("api2", fetchFromApi2) - .build(), - 5000 // 5 second timeout for entire parallel execution -); - -// Sequence with retry -auto reliable = withRetry( - sequence() - .add(fetchData) - .add(processData) - .build(), - RetryPolicy::exponential(3) -); -``` - -## Factory Functions - -| Function | Description | -|----------|-------------| -| `sequence(name)` | Create Sequence builder | -| `parallel(name)` | Create Parallel builder | -| `router(name)` | Create JSON Router builder | -| `makeRouter(name)` | Create typed Router builder | -| `makeSequence(a, b)` | Create type-safe two-step Sequence | -| `a \| b` | Pipe operator for type-safe chaining | - -## Best Practices - -1. **Name your compositions** - Use descriptive names for debugging -2. **Keep branches independent** - Parallel branches shouldn't depend on each other -3. **Handle errors at boundaries** - Use resilience wrappers where appropriate -4. **Consider timeouts** - Long-running compositions should have timeouts -5. **Test branches individually** - Unit test each component before composing - -## See Also - -- [Runnable Interface](Runnable.md) - Core interface -- [Resilience Patterns](Resilience.md) - Retry, Timeout, Fallback, CircuitBreaker -- [StateGraph Guide](StateGraph.md) - Stateful workflows with conditional edges diff --git a/third_party/gopher-orch/docs/FFI.md b/third_party/gopher-orch/docs/FFI.md deleted file mode 100644 index e2515801..00000000 --- a/third_party/gopher-orch/docs/FFI.md +++ /dev/null @@ -1,414 +0,0 @@ -# FFI Guide - -Gopher Orch provides a stable C API (FFI layer) for integration with other programming languages. Build agents in Python, Rust, Go, or any language with C FFI support. - -## Overview - -``` -┌─────────────────────────────────────────────────────────┐ -│ Your Application │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ Python │ │ Rust │ │ Go │ │ Node.js │ │ -│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ -│ │ │ │ │ │ -│ └──────────┴──────────┴──────────┘ │ -│ │ │ -│ ┌──────────┴──────────┐ │ -│ │ Language Bindings │ │ -│ └──────────┬──────────┘ │ -├─────────────────────────┼───────────────────────────────┤ -│ ┌──────────┴──────────┐ │ -│ │ C API (FFI Layer) │ │ -│ │ libgopher_orch_c │ │ -│ └──────────┬──────────┘ │ -├─────────────────────────┼───────────────────────────────┤ -│ ┌──────────┴──────────┐ │ -│ │ Gopher Orch C++ │ │ -│ └─────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - -## C API Design - -The C API uses: -- **Opaque handles** - Hide C++ implementation details -- **RAII guards** - Automatic resource cleanup -- **Error codes** - Explicit error handling -- **Callbacks** - Async operation support - -### Handle Types - -```c -// Opaque handle types -typedef struct gopher_orch_agent* gopher_orch_agent_t; -typedef struct gopher_orch_registry* gopher_orch_registry_t; -typedef struct gopher_orch_provider* gopher_orch_provider_t; -typedef struct gopher_orch_runnable* gopher_orch_runnable_t; -``` - -### Error Handling - -```c -// Error structure -typedef struct { - int code; - const char* message; -} gopher_orch_error_t; - -// Check for errors -gopher_orch_error_t err; -if (gopher_orch_agent_invoke(agent, input, &err) != 0) { - printf("Error %d: %s\n", err.code, err.message); - gopher_orch_error_free(&err); -} -``` - -## Building the C API - -```bash -# Build with C API enabled (default) -cmake -B build -DBUILD_C_API=ON -make -C build - -# Output: lib/libgopher_orch_c.{so,dylib,dll} -# Headers: include/gopher-orch/ffi/ -``` - -## Python Bindings - -### Installation - -```bash -pip install gopher-orch -``` - -### Basic Usage - -```python -from gopher_orch import Agent, ToolRegistry, OpenAIProvider - -# Create provider -provider = OpenAIProvider(api_key="sk-...") - -# Create registry with tools -registry = ToolRegistry() - -@registry.tool("search", "Search the web") -def search(query: str) -> dict: - return {"results": [...]} - -@registry.tool("calculate", "Perform calculations") -def calculate(expression: str) -> float: - return eval(expression) - -# Create agent -agent = Agent( - provider=provider, - registry=registry, - system_prompt="You are a helpful assistant." -) - -# Run agent -result = agent.invoke("What's 2+2 and search for weather in Tokyo") -print(result.response) -``` - -### Async Support - -```python -import asyncio -from gopher_orch import AsyncAgent - -async def main(): - agent = AsyncAgent(provider, registry) - - # Async invocation - result = await agent.invoke("Search for news") - - # Streaming - async for chunk in agent.stream("Tell me a story"): - print(chunk, end="", flush=True) - -asyncio.run(main()) -``` - -## Rust Bindings - -### Cargo.toml - -```toml -[dependencies] -gopher-orch = "0.1" -``` - -### Usage - -```rust -use gopher_orch::{Agent, ToolRegistry, OpenAIProvider}; - -fn main() -> Result<(), Box> { - // Create provider - let provider = OpenAIProvider::new("sk-...")?; - - // Create registry - let mut registry = ToolRegistry::new(); - - registry.add_tool("search", "Search the web", |args| { - let query = args.get("query").as_str()?; - Ok(json!({"results": search_web(query)})) - })?; - - // Create agent - let agent = Agent::builder() - .provider(provider) - .registry(registry) - .system_prompt("You are helpful.") - .build()?; - - // Run agent - let result = agent.invoke("Search for weather")?; - println!("{}", result.response); - - Ok(()) -} -``` - -## Go Bindings - -### Installation - -```bash -go get github.com/anthropics/gopher-orch-go -``` - -### Usage - -```go -package main - -import ( - "fmt" - orch "github.com/anthropics/gopher-orch-go" -) - -func main() { - // Create provider - provider := orch.NewOpenAIProvider("sk-...") - - // Create registry - registry := orch.NewToolRegistry() - - registry.AddTool("search", "Search the web", func(args orch.JSON) (orch.JSON, error) { - query := args.GetString("query") - return searchWeb(query), nil - }) - - // Create agent - agent := orch.NewAgent(provider, registry, orch.AgentConfig{ - SystemPrompt: "You are helpful.", - }) - - // Run agent - result, err := agent.Invoke("Search for news") - if err != nil { - panic(err) - } - fmt.Println(result.Response) -} -``` - -## Node.js Bindings - -### Installation - -```bash -npm install gopher-orch -``` - -### Usage - -```javascript -const { Agent, ToolRegistry, OpenAIProvider } = require('gopher-orch'); - -async function main() { - // Create provider - const provider = new OpenAIProvider({ apiKey: 'sk-...' }); - - // Create registry - const registry = new ToolRegistry(); - - registry.addTool('search', 'Search the web', async (args) => { - const results = await searchWeb(args.query); - return { results }; - }); - - // Create agent - const agent = new Agent({ - provider, - registry, - systemPrompt: 'You are helpful.' - }); - - // Run agent - const result = await agent.invoke('Search for weather'); - console.log(result.response); -} - -main(); -``` - -## C API Reference - -### Agent Functions - -```c -// Create agent -gopher_orch_agent_t gopher_orch_agent_create( - gopher_orch_provider_t provider, - gopher_orch_registry_t registry, - const char* config_json -); - -// Invoke agent (blocking) -int gopher_orch_agent_invoke( - gopher_orch_agent_t agent, - const char* input_json, - char** output_json, - gopher_orch_error_t* error -); - -// Invoke agent (async) -int gopher_orch_agent_invoke_async( - gopher_orch_agent_t agent, - const char* input_json, - gopher_orch_callback_t callback, - void* user_data -); - -// Destroy agent -void gopher_orch_agent_destroy(gopher_orch_agent_t agent); -``` - -### Registry Functions - -```c -// Create registry -gopher_orch_registry_t gopher_orch_registry_create(void); - -// Add tool -int gopher_orch_registry_add_tool( - gopher_orch_registry_t registry, - const char* name, - const char* description, - const char* schema_json, - gopher_orch_tool_fn callback, - void* user_data -); - -// Destroy registry -void gopher_orch_registry_destroy(gopher_orch_registry_t registry); -``` - -### Provider Functions - -```c -// Create OpenAI provider -gopher_orch_provider_t gopher_orch_openai_create( - const char* api_key, - const char* model -); - -// Create Anthropic provider -gopher_orch_provider_t gopher_orch_anthropic_create( - const char* api_key, - const char* model -); - -// Destroy provider -void gopher_orch_provider_destroy(gopher_orch_provider_t provider); -``` - -## Memory Management - -### RAII Guards - -The C API provides RAII-style guards for automatic cleanup: - -```c -// C++ style RAII (if available) -#include - -void example() { - GOPHER_ORCH_GUARD(agent, gopher_orch_agent_create(...)); - // agent automatically destroyed when scope exits -} -``` - -### Manual Cleanup - -```c -gopher_orch_agent_t agent = gopher_orch_agent_create(...); -// ... use agent ... -gopher_orch_agent_destroy(agent); -``` - -## Thread Safety - -- All FFI functions are thread-safe -- Callbacks may be invoked from different threads -- Use the dispatcher model for coordination - -```c -// Thread-safe invocation -gopher_orch_agent_invoke_async(agent, input, - on_complete_callback, user_data); - -// Callback may be called from any thread -void on_complete_callback(const char* result, void* user_data) { - // Handle result thread-safely -} -``` - -## Error Codes - -```c -#define GOPHER_ORCH_OK 0 -#define GOPHER_ORCH_ERR_NULL_PTR -1 -#define GOPHER_ORCH_ERR_INVALID -2 -#define GOPHER_ORCH_ERR_TIMEOUT -3 -#define GOPHER_ORCH_ERR_INTERNAL -4 -``` - -## Best Practices - -1. **Always check errors** - Every FFI call can fail -2. **Free resources** - Call destroy functions or use guards -3. **Copy strings** - FFI strings may be freed after call returns -4. **Use async APIs** - Avoid blocking the main thread -5. **Handle callbacks safely** - They may come from any thread - -## Building Custom Bindings - -For unsupported languages, use the C API directly: - -```c -// 1. Load library -void* lib = dlopen("libgopher_orch_c.so", RTLD_NOW); - -// 2. Get function pointers -typedef gopher_orch_agent_t (*create_fn)(/* ... */); -create_fn create = dlsym(lib, "gopher_orch_agent_create"); - -// 3. Call functions -gopher_orch_agent_t agent = create(/* ... */); - -// 4. Cleanup -gopher_orch_agent_destroy(agent); -dlclose(lib); -``` - -## See Also - -- [Runnable Interface](Runnable.md) - Core C++ interface -- [Agent Framework](Agent.md) - Agent implementation details -- [Server Abstraction](Server.md) - Protocol support diff --git a/third_party/gopher-orch/docs/GatewayServer.md b/third_party/gopher-orch/docs/GatewayServer.md deleted file mode 100644 index 76cbf3c3..00000000 --- a/third_party/gopher-orch/docs/GatewayServer.md +++ /dev/null @@ -1,1080 +0,0 @@ -# GatewayServer Documentation - -## Overview - -**GatewayServer** is an MCP (Model Context Protocol) server that acts as a gateway/proxy, aggregating and exposing tools from multiple backend MCP servers through a single unified endpoint. This allows clients to interact with tools from different servers as if they were all provided by one server. - -### Key Concepts - -- **Gateway Server**: The front-facing MCP server that external clients connect to -- **Backend Servers**: Multiple MCP servers (running on different ports/processes) that provide tools -- **Tool Aggregation**: The gateway discovers all tools from backend servers and exposes them through its own endpoint -- **Transparent Routing**: Tool calls are automatically routed to the appropriate backend server -- **Connection Management**: Automatic reconnection and connection pooling for backend servers - ---- - -## Architecture - -``` -External MCP Clients (e.g., Claude Desktop, ReActAgent) - | - | HTTP/SSE - v - +-------------------------+ - | GatewayServer | (Port 3003) - | (MCP Server) | - | | - | ServerComposite | - | (Tool Registry) | - +-------------------------+ - | - +-----------+-----------+ - | | - v v - Backend Server 1 Backend Server 2 - (Port 3001) (Port 3002) - - weather tools - auth tools - - time tools - user tools -``` - -### Components - -1. **GatewayServer**: The main server class that: - - Listens on a configurable port (default: 3003) - - Registers all tools from backend servers - - Routes tool calls to the appropriate backend - -2. **ServerComposite**: Internal component that: - - Manages connections to backend servers - - Maintains a unified tool registry - - Handles tool routing and execution - -3. **Backend Servers**: Individual MCP servers that: - - Provide specific tools (e.g., weather, auth, database) - - Can be added or removed dynamically - - Run independently on different ports or as stdio processes - ---- - -## Use Cases - -### 1. **Microservices Architecture** -Split your tool implementations across multiple specialized services: -- `weather-service` on port 3001 (weather, location tools) -- `auth-service` on port 3002 (authentication, user management) -- `database-service` on port 3003 (data access tools) - -The gateway exposes all tools through a single endpoint, simplifying client configuration. - -### 2. **Multi-Language Tool Ecosystem** -Combine tools written in different languages: -- Python service for data science tools -- Node.js service for web scraping -- C++ service for high-performance computation - -### 3. **Development and Testing** -- Test tool implementations in isolation while exposing them through a unified interface -- Gradually migrate tools from monolithic to microservices architecture -- Enable A/B testing by routing to different backend implementations - -### 4. **Connection Pooling and Load Balancing** -- Single connection point for clients (reduces connection overhead) -- Backend connections are maintained with automatic reconnection -- Efficient resource utilization - ---- - -## How It Works - -### Initialization Flow - -1. **Gateway Creation**: - ```cpp - auto gateway = GatewayServer::create(serverJson, config); - ``` - - Parses JSON configuration containing backend server endpoints - - Creates a ServerComposite to manage backend servers - -2. **Backend Connection**: - - Gateway creates an MCP client for each backend server - - Connects to backends (HTTP/SSE or stdio) - - Queries each backend for available tools using `tools/list` - -3. **Tool Registration**: - - Collects all tool definitions (name, description, schema) from backends - - Registers unified tool handlers on the gateway's MCP server - - Maintains mapping of tool names to backend servers - -4. **Server Startup**: - ```cpp - gateway->listen(3003); // Start and block - ``` - - Starts MCP server on configured port - - Accepts client connections - - Blocks until shutdown (Ctrl+C) - -### Request Flow - -``` -1. Client calls tool via Gateway - ↓ -2. Gateway receives tools/call request - ↓ -3. Gateway looks up which backend owns the tool - ↓ -4. Gateway routes call to backend MCP client - ↓ -5. Backend executes tool and returns result - ↓ -6. Gateway forwards result to client -``` - -### Thread Safety - -- **Gateway Server**: Thread-safe, can handle multiple concurrent clients -- **Tool Execution**: Each tool call runs in the backend's dispatcher thread -- **Synchronization**: Uses mutex and condition variables for blocking tool calls -- **Shutdown**: Graceful shutdown on SIGINT/SIGTERM, forced exit on second signal - ---- - -## API Documentation - -### Simple API (Recommended for Most Use Cases) - -#### `GatewayServer::create(serverJson, config)` - -Creates a gateway server from JSON configuration. - -**Parameters:** -- `serverJson` (string): JSON configuration of backend servers -- `config` (GatewayServerConfig, optional): Server configuration - -**Returns:** `GatewayServerPtr` (shared_ptr) - -**Example:** -```cpp -// Manifest format with metadata, config, and servers -std::string serverJson = R"({ - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01" - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2 - } - }, - "servers": [ - { - "serverId": "1877234567890123456", - "name": "weather-server", - "url": "http://127.0.0.1:3001/mcp" - }, - { - "serverId": "1877234567890123457", - "name": "auth-server", - "url": "http://127.0.0.1:3002/mcp" - } - ] -})"; - -// API response format is also supported: -// {"succeeded": true, "data": { }} - -auto gateway = GatewayServer::create(serverJson); -if (!gateway->getError().empty()) { - std::cerr << "Error: " << gateway->getError() << std::endl; - return 1; -} -``` - -#### `listen(port)` - -Starts the gateway server and blocks until shutdown. - -**Parameters:** -- `port` (int): Port to listen on - -**Returns:** `int` (0 on success, non-zero on error) - -**Example:** -```cpp -gateway->listen(3003); // Blocks until Ctrl+C -``` - -### Advanced API (For Fine-Grained Control) - -#### `GatewayServer::create(composite, config)` - -Creates a gateway server with an existing ServerComposite. - -**Parameters:** -- `composite` (ServerCompositePtr): Pre-configured server composite -- `config` (GatewayServerConfig, optional): Server configuration - -**Returns:** `GatewayServerPtr` - -**Example:** -```cpp -// Create composite manually -auto composite = ServerComposite::create("my-composite"); - -// Configure and add servers manually -MCPServerConfig server1_config; -server1_config.name = "weather-server"; -server1_config.transport_type = MCPServerConfig::TransportType::HTTP_SSE; -server1_config.http_sse_transport.url = "http://127.0.0.1:3001/mcp"; - -// Create and connect server asynchronously -MCPServer::create(server1_config, dispatcher, [&](Result result) { - if (mcp::holds_alternative(result)) { - auto server1 = mcp::get(result); - std::cout << "Server 1 connected" << std::endl; - composite->addServer(server1, {"weather", "time"}, false); - } -}); - -// Create gateway with composite -GatewayServerConfig config; -config.port = 3003; -auto gateway = GatewayServer::create(composite, config); -``` - -#### `start(dispatcher, callback)` - -Starts the gateway server asynchronously. - -**Parameters:** -- `dispatcher` (Dispatcher&): Event dispatcher for async operations -- `callback` (function): Called when server starts - -**Example:** -```cpp -gateway->start(dispatcher, [](VoidResult result) { - if (mcp::holds_alternative(result)) { - std::cout << "Gateway started successfully" << std::endl; - } else { - auto error = mcp::get(result); - std::cerr << "Gateway failed: " << error.message << std::endl; - } -}); -``` - -#### `stop(dispatcher, callback)` / `stop()` - -Stops the gateway server. - -**Parameters:** -- `dispatcher` (Dispatcher&, optional): For async stop -- `callback` (function, optional): Called when server stops - -**Example:** -```cpp -// Async stop -gateway->stop(dispatcher, []() { - std::cout << "Gateway stopped" << std::endl; -}); - -// Blocking stop -gateway->stop(); -``` - -### Accessor Methods - -```cpp -// Get server name -const std::string& name() const; - -// Get underlying ServerComposite -ServerCompositePtr getComposite() const; - -// Check if server is running -bool isRunning() const; - -// Get listen address (e.g., "0.0.0.0:3003") -std::string getListenAddress() const; - -// Get listen URL (e.g., "http://0.0.0.0:3003") -std::string getListenUrl() const; - -// Get number of registered tools -size_t toolCount() const; - -// Get number of connected backend servers -size_t serverCount() const; - -// Get error message if creation failed -const std::string& getError() const; -``` - ---- - -## Configuration - -### `GatewayServerConfig` Structure - -```cpp -struct GatewayServerConfig { - std::string name = "gateway-server"; // Server name - std::string host = "0.0.0.0"; // Listen host - int port = 3003; // Listen port - int workers = 4; // Worker threads - int max_sessions = 100; // Max concurrent sessions - std::chrono::milliseconds session_timeout{300000}; // 5 minutes - std::chrono::milliseconds request_timeout{30000}; // 30 seconds - - // HTTP/SSE endpoint paths - std::string http_rpc_path = "/mcp"; // RPC endpoint - std::string http_sse_path = "/events"; // SSE endpoint - std::string http_health_path = "/health"; // Health check -}; -``` - -### Backend Server JSON Format - -The gateway supports two JSON formats: - -#### Manifest Format (Config File or Environment Variable) - -```json -{ - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01", - "generatedAt": 1768114552523 - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2, - "retryableCodes": [429, 500, 502, 503, 504] - } - }, - "servers": [ - { - "serverId": "1877234567890123456", - "version": "2025-01-09", - "name": "server-name", - "url": "http://127.0.0.1:3001/mcp" - } - ] -} -``` - -#### API Response Format (with succeeded/data wrapper) - -```json -{ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "version": "2026-01-11", - "metadata": { ... }, - "config": { ... }, - "servers": [ ... ] - } -} -``` - -### Server Object Fields - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `serverId` | string | Yes | Unique server identifier | -| `version` | string | No | Server configuration version | -| `name` | string | Yes | Server display name | -| `url` | string | Yes* | HTTP/SSE endpoint URL | -| `command` | string | Yes* | Stdio command (alternative to url) | -| `args` | array | No | Command arguments (for stdio) | - -*Either `url` (HTTP/SSE) or `command` (stdio) is required. - -### Transport Auto-Detection - -The gateway automatically detects transport type: - -#### HTTP/SSE Transport (server has `url` field) -```json -{ - "serverId": "123", - "name": "remote-server", - "url": "http://example.com:3001/mcp" -} -``` - -#### Stdio Transport (server has `command` field) -```json -{ - "serverId": "456", - "name": "local-tool", - "command": "python", - "args": ["-m", "my_tool_server"] -} -``` - ---- - -## Examples - -### Example 1: Basic Gateway Setup - -**File: `gateway_example.cpp`** - -```cpp -#include -#include "gopher/orch/server/gateway_server.h" - -using namespace gopher::orch::server; - -int main() { - std::cout << "Starting GatewayServer..." << std::endl; - - // Configure backend servers with manifest format - std::string serverJson = R"({ - "version": "2026-01-11", - "metadata": { - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01" - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000 - }, - "servers": [ - { - "serverId": "1877234567890123456", - "name": "weather-service", - "url": "http://127.0.0.1:3001/mcp" - }, - { - "serverId": "1877234567890123457", - "name": "auth-service", - "url": "http://127.0.0.1:3002/mcp" - } - ] - })"; - - // Create gateway - auto gateway = GatewayServer::create(serverJson); - - if (!gateway->getError().empty()) { - std::cerr << "Failed to create gateway: " - << gateway->getError() << std::endl; - return 1; - } - - std::cout << "Gateway created successfully" << std::endl; - std::cout << "Registered " << gateway->toolCount() << " tools" << std::endl; - std::cout << "Connected to " << gateway->serverCount() << " servers" << std::endl; - std::cout << "Listening on " << gateway->getListenUrl() << std::endl; - - // Start listening (blocks until Ctrl+C) - return gateway->listen(3003); -} -``` - -### Example 2: Custom Configuration - -```cpp -#include "gopher/orch/server/gateway_server.h" - -int main() { - std::string serverJson = "..."; // Backend configuration - - // Custom gateway configuration - GatewayServerConfig config; - config.name = "my-gateway"; - config.host = "127.0.0.1"; // Localhost only - config.port = 8080; - config.workers = 8; - config.max_sessions = 200; - config.session_timeout = std::chrono::minutes(10); - config.request_timeout = std::chrono::seconds(60); - config.http_rpc_path = "/api/mcp"; - config.http_sse_path = "/api/events"; - - auto gateway = GatewayServer::create(serverJson, config); - return gateway->listen(config.port); -} -``` - -### Example 3: Dynamic Server Management - -```cpp -#include "gopher/orch/server/gateway_server.h" -#include "gopher/orch/server/server_composite.h" -#include "gopher/orch/server/mcp_server.h" -#include "mcp/event/libevent_dispatcher.h" - -int main() { - // Create composite manually - auto composite = ServerComposite::create("dynamic-gateway"); - - // Create dispatcher - mcp::event::LibeventDispatcher dispatcher("gateway"); - - // Add server 1 - MCPServerConfig config1; - config1.name = "server1"; - config1.transport_type = MCPServerConfig::TransportType::HTTP_SSE; - config1.http_sse_transport.url = "http://localhost:3001/mcp"; - - MCPServer::create(config1, dispatcher, [&](Result result) { - if (mcp::holds_alternative(result)) { - auto server1 = mcp::get(result); - - // Discover tools - server1->listTools(dispatcher, [&, server1](auto tools_result) { - if (!isError>(tools_result)) { - auto tools = mcp::get>(tools_result); - std::vector tool_names; - for (const auto& tool : tools) { - tool_names.push_back(tool.name); - } - - // Add to composite - composite->addServer(server1, tool_names, false); - std::cout << "Added server1 with " << tool_names.size() - << " tools" << std::endl; - } - }); - } - }); - - // Run dispatcher to process async operations - dispatcher.run(mcp::event::RunType::NonBlock); - - // Create gateway with composite - auto gateway = GatewayServer::create(composite); - - // Later: dynamically add another server - // composite->addServer(server2, tool_names2, false); - - return gateway->listen(3003); -} -``` - -### Example 4: Testing Gateway with Client - -**File: `gateway_client_test.cpp`** - -```cpp -#include -#include "gopher/orch/agent/agent.h" - -using namespace gopher::orch::agent; - -int main() { - std::cout << "=== Gateway Client Test ===" << std::endl; - - // Connect to gateway (not backend servers directly) - std::string gatewayConfig = R"({ - "version": "2026-01-11", - "metadata": { - "gatewayId": "client-test" - }, - "config": {}, - "servers": [ - { - "serverId": "1", - "name": "gateway", - "url": "http://127.0.0.1:3003/mcp" - } - ] - })"; - - // Create agent connected to gateway - auto agent = ReActAgent::createByJson( - "AnthropicProvider", - "claude-3-5-sonnet-20241022", - gatewayConfig - ); - - if (!agent) { - std::cerr << "Failed to create agent" << std::endl; - return 1; - } - - std::cout << "Agent connected to gateway" << std::endl; - - // Test queries that use tools from different backends - std::vector queries = { - "What is the weather in Tokyo?", // weather-service tool - "List all registered users", // auth-service tool - "What time is it in New York?" // time-service tool - }; - - for (const auto& query : queries) { - std::cout << "\nQuery: " << query << std::endl; - std::string answer = agent->run(query); - std::cout << "Answer: " << answer << std::endl; - } - - return 0; -} -``` - ---- - -## Features and Benefits - -### Key Features - -1. **Tool Aggregation** - - Automatic discovery of tools from all backend servers - - Unified tool registry accessible through single endpoint - - Preserves tool metadata (descriptions, schemas, examples) - -2. **Transparent Routing** - - Automatic routing of tool calls to correct backend - - No client-side knowledge of backend topology required - - Support for duplicate tool names (last-registered wins) - -3. **Connection Management** - - Automatic reconnection to backend servers - - Connection pooling and reuse - - Idle timeout detection (4-second threshold) - - Retry logic with exponential backoff (max 50 retries × 10ms) - -4. **Transport Flexibility** - - HTTP/SSE transport for remote servers - - Stdio transport for local processes - - Mixed transport types in same gateway - -5. **Robustness** - - Graceful shutdown on SIGINT/SIGTERM - - Forced exit on second signal - - Backend failure isolation (one backend failure doesn't affect others) - - Comprehensive debug logging - -6. **Performance** - - Non-blocking I/O with libevent - - Multi-threaded request handling - - Efficient connection reuse - - Configurable worker threads - -### Benefits - -#### For Developers -- **Modularity**: Split tool implementations into focused services -- **Scalability**: Scale backend services independently -- **Language Freedom**: Use different languages for different tools -- **Testing**: Test services in isolation -- **Development Velocity**: Teams can work on different backends independently - -#### For Clients -- **Simplicity**: Single endpoint to connect to -- **Consistency**: Uniform interface across all tools -- **Performance**: Connection pooling reduces overhead -- **Reliability**: Automatic reconnection and retry logic - -#### For Operations -- **Monitoring**: Centralized access logs and metrics -- **Deployment**: Independent deployment of backend services -- **Load Balancing**: Route to different backends for load distribution -- **Security**: Single point for authentication/authorization - ---- - -## Best Practices - -### 1. Backend Server Organization - -``` -Organize by domain/functionality: - -✓ GOOD: Domain-based separation - - weather-service (weather, location, time zone) - - auth-service (users, sessions, permissions) - - data-service (database, cache, search) - -✗ AVOID: Fine-grained separation - - tool1-service (single tool) - - tool2-service (single tool) - - tool3-service (single tool) -``` - -### 2. Error Handling - -```cpp -// Always check for creation errors -auto gateway = GatewayServer::create(serverJson); -if (!gateway->getError().empty()) { - LOG_ERROR("Gateway creation failed: " << gateway->getError()); - // Handle error appropriately - return 1; -} - -// Check connection status -if (gateway->serverCount() == 0) { - LOG_WARNING("No backend servers connected"); -} - -// Log tool registration -LOG_INFO("Gateway registered " << gateway->toolCount() << " tools " - << "from " << gateway->serverCount() << " servers"); -``` - -### 3. Configuration Management - -```cpp -// Load configuration from file -std::string loadServerConfig(const std::string& filename) { - std::ifstream file(filename); - if (!file.is_open()) { - throw std::runtime_error("Failed to open " + filename); - } - return std::string( - std::istreambuf_iterator(file), - std::istreambuf_iterator() - ); -} - -// Use environment-specific configs -std::string env = std::getenv("ENVIRONMENT") ?: "development"; -std::string configFile = "config." + env + ".json"; -std::string serverJson = loadServerConfig(configFile); - -auto gateway = GatewayServer::create(serverJson); -``` - -### 4. Monitoring and Logging - -```cpp -// Enable debug logging -export GOPHER_LOG_LEVEL=debug - -// Log gateway state periodically -void logGatewayStats(GatewayServerPtr gateway) { - LOG_INFO("Gateway Stats:"); - LOG_INFO(" Running: " << gateway->isRunning()); - LOG_INFO(" Tools: " << gateway->toolCount()); - LOG_INFO(" Servers: " << gateway->serverCount()); - LOG_INFO(" Address: " << gateway->getListenUrl()); -} -``` - -### 5. Graceful Shutdown - -```cpp -// The gateway handles SIGINT/SIGTERM automatically -// For custom cleanup: - -class Application { - GatewayServerPtr gateway_; - - void setupSignalHandlers() { - signal(SIGINT, [](int) { - LOG_INFO("Shutdown requested..."); - // Gateway auto-shuts down - // Add custom cleanup here - }); - } - - int run() { - setupSignalHandlers(); - return gateway_->listen(3003); - } -}; -``` - -### 6. Testing Strategy - -```cpp -// Unit test: Mock ServerComposite -TEST(GatewayServerTest, CreatesFromComposite) { - auto composite = ServerComposite::create("test"); - auto gateway = GatewayServer::create(composite); - EXPECT_TRUE(gateway != nullptr); -} - -// Integration test: Real backend servers -TEST(GatewayServerTest, ConnectsToBackends) { - // Start real backend servers on test ports - startTestServer(4001, {"tool1", "tool2"}); - startTestServer(4002, {"tool3", "tool4"}); - - std::string config = makeConfig({ - {"server1", "http://localhost:4001/mcp"}, - {"server2", "http://localhost:4002/mcp"} - }); - - auto gateway = GatewayServer::create(config); - EXPECT_EQ(2, gateway->serverCount()); - EXPECT_EQ(4, gateway->toolCount()); -} - -// End-to-end test: Full client workflow -TEST(GatewayServerTest, ClientCanCallTools) { - auto gateway = startGatewayWithBackends(); - auto client = createTestClient("http://localhost:3003/mcp"); - - auto result = client->callTool("tool1", {}); - EXPECT_TRUE(result.succeeded); -} -``` - ---- - -## Troubleshooting - -### Common Issues - -#### 1. Gateway Creation Fails - -**Symptom:** -```cpp -auto gateway = GatewayServer::create(serverJson); -if (!gateway->getError().empty()) { - std::cerr << gateway->getError() << std::endl; - // "No 'servers' array found in configuration" -} -``` - -**Solutions:** -- Verify JSON is valid: `echo $JSON | jq .` -- Check JSON structure has `data.servers` array -- Ensure at least one server is configured -- Validate server URLs are accessible - -#### 2. No Tools Registered - -**Symptom:** -```cpp -std::cout << gateway->toolCount() << std::endl; // Prints: 0 -``` - -**Solutions:** -- Check backend servers are running -- Verify backend URLs are correct -- Check network connectivity: `curl http://localhost:3001/mcp` -- Enable debug logging: `export GOPHER_LOG_LEVEL=debug` -- Check backend servers implement `tools/list` method - -#### 3. Tool Calls Timeout - -**Symptom:** -Client calls tool, but request times out after 30 seconds. - -**Solutions:** -- Check backend server is responsive: `curl -X POST http://localhost:3001/mcp` -- Increase request timeout in configuration: - ```cpp - GatewayServerConfig config; - config.request_timeout = std::chrono::seconds(60); - ``` -- Check backend logs for errors -- Verify tool implementation doesn't hang - -#### 4. Connection Refused - -**Symptom:** -``` -Error: Connection refused to http://localhost:3001/mcp -``` - -**Solutions:** -- Start backend servers before gateway -- Check port numbers match configuration -- Verify firewall rules allow connections -- Check backend is listening: `netstat -an | grep 3001` - -#### 5. Duplicate Tool Names - -**Symptom:** -Multiple backends provide same tool name, only one works. - -**Behavior:** -Last-registered backend wins for duplicate tool names. - -**Solutions:** -- Use unique tool names across backends -- Or: Accept last-registered wins behavior -- Or: Namespace tools by backend: `server1.tool`, `server2.tool` - -#### 6. Gateway Won't Shutdown - -**Symptom:** -Press Ctrl+C, but gateway doesn't stop. - -**Solutions:** -- Press Ctrl+C again for forced exit -- Check for hanging backend connections -- Verify no long-running tool calls -- Use `kill -9` as last resort - -### Debug Logging - -Enable comprehensive debug logging: - -```bash -export GOPHER_LOG_LEVEL=debug -./gateway_server_example -``` - -This shows: -- Backend connection attempts -- Tool discovery and registration -- Request routing decisions -- Backend responses -- Error details - -Example output: -``` -[DEBUG] Gateway: Connecting to server: weather-service -[DEBUG] Gateway: Connected to http://localhost:3001/mcp -[DEBUG] Gateway: Discovered 5 tools from weather-service -[DEBUG] Gateway: Registering tool: get_weather -[DEBUG] Gateway: Registering tool: get_forecast -[DEBUG] Gateway: Gateway initialization complete -[DEBUG] Gateway: Listening on http://0.0.0.0:3003 -[DEBUG] Gateway: Tool call: get_weather -[DEBUG] Gateway: Routing to backend: weather-service -[DEBUG] Gateway: Backend response received -[DEBUG] Gateway: Tool call completed successfully -``` - -### Performance Tuning - -If experiencing performance issues: - -1. **Increase worker threads:** - ```cpp - config.workers = 16; // Default: 4 - ``` - -2. **Increase max sessions:** - ```cpp - config.max_sessions = 500; // Default: 100 - ``` - -3. **Adjust timeouts:** - ```cpp - config.session_timeout = std::chrono::minutes(10); - config.request_timeout = std::chrono::seconds(120); - ``` - -4. **Monitor connections:** - ```bash - watch -n 1 'netstat -an | grep 3003 | wc -l' - ``` - ---- - -## Advanced Topics - -### Custom Tool Routing Logic - -By default, the gateway routes tools based on registration order (last wins for duplicates). For custom routing: - -```cpp -// Create custom composite with routing logic -class CustomComposite : public ServerComposite { - CallToolResult callTool(const std::string& name, ...) override { - // Custom routing logic - if (name.starts_with("premium_")) { - return premium_server_->callTool(name, ...); - } else { - return default_server_->callTool(name, ...); - } - } -}; - -auto composite = std::make_shared(); -auto gateway = GatewayServer::create(composite); -``` - -### Tool Name Namespacing - -Automatically namespace tools by backend: - -```cpp -// When adding servers to composite -composite->addServer(server1, tool_names1, true); // true = add prefix -// Tools become: server1.weather, server1.time, etc. -``` - -### Health Checks - -The gateway provides a health check endpoint: - -```bash -curl http://localhost:3003/health -``` - -Response: -```json -{ - "status": "healthy", - "uptime_seconds": 3600, - "tool_count": 42, - "server_count": 3 -} -``` - -### Metrics and Monitoring - -Integrate with monitoring systems: - -```cpp -// Expose metrics endpoint -gateway->registerMetricsHandler([](auto& ctx) { - nlohmann::json metrics = { - {"tool_calls_total", tool_call_counter}, - {"tool_calls_errors", error_counter}, - {"backend_connection_errors", connection_error_counter}, - {"active_sessions", gateway->getActiveSessionCount()} - }; - ctx.respond(200, metrics.dump()); -}); -``` - ---- - -## See Also - -- [ServerComposite Documentation](ServerComposite.md) - Managing multiple backend servers -- [MCPServer Documentation](MCPServer.md) - Individual MCP server implementation -- [ReActAgent Documentation](ReActAgent.md) - Client agent that uses tools -- [MCP Protocol Specification](https://spec.modelcontextprotocol.io/) - Official MCP protocol docs - ---- - -## Changelog - -### Version 1.0.0 (2026-01-14) - -**Initial Release:** -- Simple API (`create` + `listen`) -- Advanced API (`create` + `start`/`stop`) -- HTTP/SSE and stdio transport support -- Automatic tool discovery and registration -- Automatic reconnection with retry logic -- Graceful shutdown handling -- Comprehensive debug logging -- Thread-safe operations -- Health check endpoint - ---- - -## License - -Copyright © 2026 Gopher Security. All rights reserved. diff --git a/third_party/gopher-orch/docs/JsonToAgentPipeline.md b/third_party/gopher-orch/docs/JsonToAgentPipeline.md deleted file mode 100644 index f39c4e9b..00000000 --- a/third_party/gopher-orch/docs/JsonToAgentPipeline.md +++ /dev/null @@ -1,1556 +0,0 @@ -# JSON-to-Agent Pipeline: Layered Architecture with ToolRegistry and ServerComposite - -## Overview - -The JSON-to-Agent Pipeline uses a **layered architecture** combining ToolRegistry and ServerComposite to provide a robust, scalable solution for tool management. This approach leverages: - -- **ServerComposite Layer**: Aggregates and manages multiple MCP servers at the infrastructure level -- **ToolRegistry Layer**: Provides agent-friendly tool interface with the composite as a unified backend -- **Clean Separation**: Infrastructure concerns (servers) separated from application concerns (agents) -- **Dynamic Discovery**: Tools discovered at runtime from MCP servers through the composite -- **Unified Execution**: All tool calls routed efficiently through the composite layer - -## Architecture Components - -### High-Level Layered Architecture - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Application Layer (Client) │ -│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │ -│ │ LLMProvider │ │ ReActAgent │ │ Business Logic │ │ -│ │ (Anthropic) │ │ │ │ (User's Code) │ │ -│ └─────────────┘ └──────┬───────┘ └─────────────────────────┘ │ -└──────────────────────────┼─────────────────────────────────────────┘ - │ uses - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Agent Interface Layer │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ ToolRegistry │ │ -│ │ • Agent-facing tool repository │ │ -│ │ • Stores tool metadata and specs │ │ -│ │ • Delegates execution to ServerComposite │ │ -│ │ • Manages local tools + server tools │ │ -│ └────────────────────────┬─────────────────────────────────────┘ │ -└───────────────────────────┼─────────────────────────────────────────┘ - │ uses single composite - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ ServerComposite │ │ -│ │ • Aggregates all MCP servers │ │ -│ │ • Namespace management (server1.tool, server2.tool) │ │ -│ │ • Connection pooling and lifecycle │ │ -│ │ • Efficient routing via CompositeServerTool │ │ -│ └────────────────────────┬─────────────────────────────────────┘ │ -│ │ manages & routes │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ MCPServer Instances (Infrastructure) │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ -│ │ │ MCP 1 │ │ MCP 2 │ │ MCP 3 │ │ MCP N │ │ │ -│ │ │ calc,math│ │ weather │ │ database │ │ email │ │ │ -│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ HTTP/SSE connections - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ External MCP Servers │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Calculator │ │ Weather API │ │ Database │ │ -│ │ Server │ │ Server │ │ Server │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### Detailed Flow Diagram - -``` -┌──────────────────┐ -│ Configuration │ ← Step 1: Load configuration -│ Source │ (Remote API or Local JSON) -└────────┬─────────┘ - │ Returns JSON config - ▼ -┌──────────────────┐ -│ ToolsFetcher │ ← Step 2: Process configuration -│ .loadFromJson() │ Parse server definitions -└────────┬─────────┘ - │ For each server - ▼ -┌──────────────────┐ -│ MCPServer │ ← Step 3: Create server instances -│ ::create() │ Initialize HTTP/SSE transport -└────────┬─────────┘ - │ Connect & discover - ▼ -┌──────────────────┐ -│ Tool Discovery │ ← Step 4: Discover available tools -│ POST /tools │ Fetch tool definitions from each server -└────────┬─────────┘ - │ Tool definitions - ▼ -┌──────────────────┐ -│ ServerComposite │ ← Step 5: Build composite -│ .addServer() │ Aggregate all servers with namespace management -└────────┬─────────┘ - │ Infrastructure ready - ▼ -┌──────────────────┐ -│ ToolRegistry │ ← Step 6: Create registry with composite backend -│ .addServer() │ Register composite as single unified backend -└────────┬─────────┘ - │ Ready for use - ▼ -┌──────────────────┐ -│ ReActAgent │ ← Step 7: Agent uses tools via registry -│ .run() │ Execute tasks with discovered tools -└────────┬─────────┘ - │ Tool execution request - ▼ -┌──────────────────┐ -│ ToolRegistry │ ← Step 8: Registry delegates to composite -│ .executeTool() │ Routes through ServerComposite -└────────┬─────────┘ - │ Delegates to composite - ▼ -┌──────────────────┐ -│ ServerComposite │ ← Step 9: Composite routes to correct server -│ .callTool() │ Efficient namespace-based routing -└────────┬─────────┘ - │ Routes to specific server - ▼ -┌──────────────────┐ -│ MCPServer │ ← Step 10: Execute on target server -│ HTTP POST │ Actual tool execution via MCP protocol -└──────────────────┘ -``` - -## Configuration Formats - -### Modern Format (Recommended) - -```json -{ - "name": "production-config", - "description": "Production tool configuration", - "mcp_servers": [ - { - "name": "calculator-server", - "transport": "http_sse", - "http_sse": { - "url": "http://calculator.example.com:3001", - "headers": { - "Authorization": "Bearer ${CALC_API_KEY}" - } - }, - "connect_timeout_ms": 10000, - "request_timeout_ms": 30000 - }, - { - "name": "weather-server", - "transport": "http_sse", - "http_sse": { - "url": "http://weather.example.com:3002", - "headers": { - "API-Key": "${WEATHER_API_KEY}" - } - }, - "connect_timeout_ms": 5000, - "request_timeout_ms": 15000 - } - ], - "auth_presets": { - "anthropic": { - "type": "bearer", - "value": "${ANTHROPIC_API_KEY}" - }, - "openai": { - "type": "bearer", - "value": "${OPENAI_API_KEY}" - } - } -} -``` - -## Core Components - Reusing Existing Infrastructure - -The JSON-to-Agent pipeline leverages existing components from gopher-orch, requiring minimal new code: - -### 1. ConfigLoader - Existing Configuration Parser - -The SDK uses the existing `ConfigLoader` from `gopher/orch/agent/config_loader.h`: - -```cpp -namespace gopher::orch::agent { - -// ConfigLoader - Complete JSON configuration parsing -// See: include/gopher/orch/agent/config_loader.h -class ConfigLoader { -public: - // Load configuration from file - Result loadFromFile(const std::string& path); - - // Load configuration from JSON string - Result loadFromString(const std::string& json_string); - - // Environment variable substitution (handles ${VAR_NAME}) - std::string substituteEnvVars(const std::string& input) const; - - // Parse individual components - Result parseToolDefinition(const JsonValue& json); - Result parseMCPServerDefinition(const JsonValue& json); - Result parseAuthPreset(const JsonValue& json); -}; - -// Complete configuration structure -struct RegistryConfig { - std::string name = "tool-registry"; - std::string base_url; - std::map default_headers; - std::map auth_presets; - std::vector mcp_servers; - std::vector tools; -}; - -} // namespace gopher::orch::agent -``` - -### 2. ToolRegistry - Existing Agent Integration - -The SDK uses the existing `ToolRegistry` from `gopher/orch/agent/tool_registry.h`: - -```cpp -namespace gopher::orch::agent { - -// ToolRegistry - Complete tool management for agents -// See: include/gopher/orch/agent/tool_registry.h -class ToolRegistry { -public: - // Load from configuration file - void loadFromFile(const std::string& path, - Dispatcher& dispatcher, - std::function callback); - - // Load from configuration object - void loadConfig(const RegistryConfig& config, - Dispatcher& dispatcher, - std::function callback); - - // Add MCP servers dynamically - void addMCPServer(const MCPServerDefinition& def, - Dispatcher& dispatcher, - std::function callback); - - // Tool access for agents - size_t toolCount() const; - std::vector getToolNames() const; - std::vector getToolSpecs() const; - optional getToolEntry(const std::string& name) const; - - // Execute tool - void executeTool(const std::string& name, - const JsonValue& arguments, - Dispatcher& dispatcher, - JsonCallback callback); - - // Get MCP servers - std::map getMCPServers() const; - MCPServerPtr getMCPServer(const std::string& name) const; -}; - -} // namespace gopher::orch::agent -``` - -### 3. ServerComposite - Existing Server Aggregation - -The SDK uses the existing `ServerComposite` from `gopher/orch/server/server_composite.h`: - -```cpp -namespace gopher::orch::server { - -// ServerComposite - Aggregates multiple servers -// See: include/gopher/orch/server/server_composite.h -class ServerComposite : public Server { -public: - // Create composite with name - static std::shared_ptr create(const std::string& name); - - // Add servers to composite - void addServer(const std::string& name, - std::shared_ptr server, - bool use_prefix = true); - - // Add server with specific tool names - void addServer(const std::string& name, - std::shared_ptr server, - const std::vector& tool_names, - bool use_prefix = true); - - // Server interface implementation - void listTools(Dispatcher& dispatcher, - std::function>)> callback) override; - - void callTool(const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) override; - - // Get all registered servers - std::map> getServers() const; -}; - -} // namespace gopher::orch::server -``` - -### 4. ToolsFetcher - Thin Orchestration Layer - -The ToolsFetcher coordinates the layered architecture: - -```cpp -namespace gopher::orch::sdk { - -// ToolsFetcher - Orchestrates the layered architecture -class ToolsFetcher { -public: - // Load configuration and build layers - void loadFromJson(const std::string& json_config, - Dispatcher& dispatcher, - std::function callback) { - // Step 1: Parse configuration - auto config_result = config_loader_.loadFromString(json_config); - if (!isSuccess(config_result)) { - callback(VoidResult(getError(config_result))); - return; - } - auto config = getValue(config_result); - - // Step 2: Create infrastructure layer (ServerComposite) - composite_ = ServerComposite::create("ToolComposite"); - - // Step 3: Create and connect MCP servers - auto pending = std::make_shared>(config.mcp_servers.size()); - auto servers = std::make_shared>>(); - - for (const auto& server_def : config.mcp_servers) { - MCPServer::create(convertToMCPConfig(server_def), dispatcher, - [=](Result result) { - if (isSuccess(result)) { - auto server = getValue(result); - servers->push_back({server_def.name, server}); - } - - if (--(*pending) == 0) { - // Step 4: Add all servers to composite with tool discovery - for (const auto& [name, server] : *servers) { - server->listTools(dispatcher, - [=](Result> tools_result) { - if (isSuccess(tools_result)) { - auto tools = getValue(tools_result); - std::vector tool_names; - for (const auto& tool : tools) { - tool_names.push_back(tool.name); - } - composite_->addServer(name, server, tool_names, true); - } - }); - } - - // Step 5: Create application layer (ToolRegistry) with composite backend - registry_ = std::make_shared(); - registry_->addServer(composite_, dispatcher); - - callback(VoidResult(nullptr)); - } - }); - } - } - - // Get the layered components - std::shared_ptr getRegistry() const { return registry_; } - std::shared_ptr getComposite() const { return composite_; } - -private: - ConfigLoader config_loader_; - std::shared_ptr composite_; // Infrastructure layer - std::shared_ptr registry_; // Application layer -}; - -} // namespace gopher::orch::sdk -``` - -### 5. MCPServer - Existing MCP Protocol Implementation - -The SDK uses the existing `MCPServer` class from `gopher/orch/server/mcp_server.h` which already supports HTTP+SSE transport: - -```cpp -namespace gopher::orch::server { - -// MCPServer configuration for HTTP+SSE transport -struct MCPServerConfig { - std::string name; // Server name - - // HTTP+SSE transport configuration - struct HttpSseTransport { - std::string url; // Server URL - std::map headers; // HTTP headers - bool verify_ssl = true; // SSL verification - }; - - TransportType transport_type = TransportType::HTTP_SSE; - HttpSseTransport http_sse_transport; - - // Timeouts - std::chrono::milliseconds connect_timeout{30000}; - std::chrono::milliseconds request_timeout{60000}; -}; - -// MCPServer - Full MCP protocol implementation -// See: include/gopher/orch/server/mcp_server.h -class MCPServer : public Server { -public: - // Factory method - static void create( - const MCPServerConfig& config, - Dispatcher& dispatcher, - std::function)> callback, - bool auto_connect = true); - - // Server interface - std::string name() const override; - ConnectionState connectionState() const override; - - void connect(Dispatcher& dispatcher, ConnectionCallback callback) override; - void disconnect(Dispatcher& dispatcher, std::function callback) override; - - void listTools(Dispatcher& dispatcher, ServerToolListCallback callback) override; - - JsonRunnablePtr tool(const std::string& name) override; - - void callTool( - const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) override; -}; - -} // namespace gopher::orch::server -``` - -Key features of the existing MCPServer: -- **Multiple Transports**: Supports stdio, HTTP+SSE, and WebSocket -- **Auto-connection**: Can automatically connect on creation -- **Tool Caching**: Caches tool information and runnables -- **Full MCP Protocol**: Complete implementation of MCP specification -- **Built-in Retries**: Configurable connection retry logic - -### 3. ServerComposite - Multi-Server Manager - -The SDK uses the existing `ServerComposite` class from `gopher/orch/server/server_composite.h`: - -```cpp -namespace gopher::orch::server { - -// ServerComposite aggregates tools from multiple servers -// See: include/gopher/orch/server/server_composite.h -class ServerComposite : public std::enable_shared_from_this { -public: - using Ptr = std::shared_ptr; - - // Factory method - static Ptr create(const std::string& name); - - // Add servers and their tools - ServerComposite& addServer(ServerPtr server, bool namespace_tools = true); - - ServerComposite& addServer( - ServerPtr server, - const std::vector& tool_names, - bool namespace_tools = true); - - ServerComposite& addServerWithAliases( - ServerPtr server, - const std::map& aliases); - - // Get tool by name (supports namespaced and aliased names) - JsonRunnablePtr tool(const std::string& name); - JsonRunnablePtr tool(const std::string& server_name, const std::string& tool_name); - - // List available tools - std::vector listTools() const; - std::vector listToolInfos() const; - - // Connection management - void connectAll( - Dispatcher& dispatcher, - std::function)> callback); - - void disconnectAll( - Dispatcher& dispatcher, - std::function callback); - - // Server management - const std::map& servers() const; - ServerPtr server(const std::string& name) const; - bool hasTool(const std::string& name) const; - void removeServer(const std::string& server_name); -}; - -} // namespace gopher::orch::server -``` - -Key features of the existing ServerComposite: -- **Tool Namespacing**: Tools can be namespaced as `server.tool` or exposed directly -- **Tool Aliasing**: Support for custom tool names via aliases -- **Lazy Connection**: Servers connect when their tools are first used -- **Automatic Routing**: Routes tool calls to the appropriate server -- **Caching**: Caches tool runnables for performance - -### 4. HttpClient - Existing HTTP Communication - -The SDK uses the existing `HttpClient` from `gopher/orch/server/rest_server.h`: - -```cpp -namespace gopher::orch::server { - -// HttpClient - Existing HTTP client implementation -// See: include/gopher/orch/server/rest_server.h -class HttpClient { -public: - virtual void request(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - ResponseCallback callback) = 0; -}; - -// DefaultHttpClient - Production HTTP client implementation -class DefaultHttpClient : public HttpClient { - // Full HTTP client with connection pooling, SSL, retries -}; - -} // namespace gopher::orch::server -``` - -### 5. Complete Infrastructure Already Available - -The existing gopher-orch codebase provides: - -- **ConfigLoader**: JSON parsing with environment variable substitution ✅ -- **ToolRegistry**: Complete agent-tool integration with MCP server management ✅ -- **MCPServer**: Full MCP protocol implementation with HTTP+SSE ✅ -- **ServerComposite**: Multi-server aggregation and routing ✅ -- **HttpClient**: HTTP communication for remote API calls ✅ -- **Tool Definition Structures**: Complete configuration schemas ✅ -- **Connection Management**: Robust lifecycle handling ✅ - -**Result: 95% of the JSON-to-Agent pipeline already exists!** - -## Tool Discovery Protocol - -### MCP Tool Discovery Request - -```http -POST /tools HTTP/1.1 -Host: localhost:3001 -Content-Type: application/json -Accept: application/json - -{ - "jsonrpc": "2.0", - "method": "tools/list", - "params": {}, - "id": "discover-001" -} -``` - -### MCP Tool Discovery Response - -```json -{ - "jsonrpc": "2.0", - "result": { - "tools": [ - { - "name": "calculator.add", - "description": "Add two numbers", - "inputSchema": { - "type": "object", - "properties": { - "a": {"type": "number", "description": "First number"}, - "b": {"type": "number", "description": "Second number"} - }, - "required": ["a", "b"] - } - }, - { - "name": "calculator.multiply", - "description": "Multiply two numbers", - "inputSchema": { - "type": "object", - "properties": { - "a": {"type": "number", "description": "First number"}, - "b": {"type": "number", "description": "Second number"} - }, - "required": ["a", "b"] - } - }, - { - "name": "weather.get_current", - "description": "Get current weather for a location", - "inputSchema": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City name or coordinates"}, - "units": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "default": "celsius" - } - }, - "required": ["location"] - } - } - ] - }, - "id": "discover-001" -} -``` - -## Tool Execution Flow - -### 1. Agent Requests Tool Execution - -```cpp -// Agent calls ToolRegistry -registry->executeTool( - "calculator.multiply", - JsonValue::object({{"a", 25}, {"b", 4}}), - dispatcher, - [](Result result) { - // Handle result - }); -``` - -### 2. Registry Delegates to ServerComposite - -```cpp -void CompositeToolRegistry::executeTool( - const std::string& name, - const JsonValue& arguments, - Dispatcher& dispatcher, - JsonCallback callback) { - // Delegate to composite - composite_->callTool(name, arguments, dispatcher, callback); -} -``` - -### 3. ServerComposite Routes to Correct Server - -```cpp -// ServerComposite internally resolves tool names and routes to the correct server -// via the CompositeServerTool wrapper: - -class CompositeServerTool : public JsonRunnable { - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Route to the appropriate server - server_->callTool(tool_name_, input, config, dispatcher, - std::move(callback)); - } -}; - -// When executeTool is called, ServerComposite: -// 1. Resolves the tool name to find the server -// 2. Creates or retrieves a cached CompositeServerTool -// 3. The tool wrapper handles routing to the correct server -``` - -### 4. MCP Server Executes Tool - -```http -POST /tool/calculator.multiply HTTP/1.1 -Host: localhost:3001 -Content-Type: application/json - -{ - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "calculator.multiply", - "arguments": { - "a": 25, - "b": 4 - } - }, - "id": "exec-001" -} -``` - -### 5. Response Flows Back - -```json -{ - "jsonrpc": "2.0", - "result": { - "result": 100, - "metadata": { - "execution_time_ms": 2, - "server": "calculator-server" - } - }, - "id": "exec-001" -} -``` - -## Client Application Usage - -### Basic Integration - Layered Architecture - -```cpp -#include "gopher/orch/sdk/tools_fetcher.h" -#include "gopher/orch/server/server_composite.h" -#include "gopher/orch/server/mcp_server.h" -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/agent/react_agent.h" -#include "gopher/orch/llm/anthropic_provider.h" - -using namespace gopher::orch; - -int main() { - // 1. Create dispatcher - auto dispatcher = std::make_shared("main"); - - // 2. Option A: Use ToolsFetcher for automatic layering - auto fetcher = std::make_shared(); - - bool ready = false; - fetcher->loadFromJson(config_json, *dispatcher, - [&ready](VoidResult result) { - if (isSuccess(result)) { - ready = true; - } - }); - - // Run event loop until ready - while (!ready) { - dispatcher->run(mcp::event::RunType::NonBlock); - } - - // Get the configured layers - auto registry = fetcher->getRegistry(); // Application layer - auto composite = fetcher->getComposite(); // Infrastructure layer - - // 3. Create LLM provider - auto provider = llm::createAnthropicProvider( - getenv("ANTHROPIC_API_KEY"), - "claude-3-opus-20240229"); - - // 5. Create agent with tools - auto agent = agent::ReActAgent::create( - provider, - registry, - agent::AgentConfig() - .withSystemPrompt("You are a helpful assistant with access to tools.") - .withMaxIterations(10)); - - // 6. Run agent with task - agent->run( - "What's the weather in Tokyo and calculate 25 * 4?", - *dispatcher, - [](Result result) { - if (isSuccess(result)) { - auto& r = getSuccess(result); - std::cout << "Response: " << r.response << "\n"; - } - }); - - // 7. Run event loop - dispatcher->run(); - - return 0; -} -``` - -### Option B: Manual Layered Setup - -```cpp -// Manually build the layered architecture for more control - -// 1. Create infrastructure layer (ServerComposite) -auto composite = server::ServerComposite::create("ToolComposite"); - -// 2. Create and add MCP servers -for (const auto& server_config : server_configs) { - server::MCPServer::create(server_config, *dispatcher, - [composite, name = server_config.name](Result result) { - if (isSuccess(result)) { - auto server = getValue(result); - - // Discover tools and add to composite - server->listTools(*dispatcher, - [composite, server, name](Result> tools) { - if (isSuccess(tools)) { - std::vector tool_names; - for (const auto& tool : getValue(tools)) { - tool_names.push_back(tool.name); - } - composite->addServer(name, server, tool_names, true); - } - }); - } - }); -} - -// 3. Create application layer (ToolRegistry) with composite backend -auto registry = agent::ToolRegistry::create(); -registry->addServer(composite, *dispatcher); - -// 4. Use with agent -auto agent = agent::ReActAgent::create(provider, registry, config); -``` - -### Advanced Integration with Remote Config - -```cpp -// Remote configuration with layered architecture -class RemoteConfigLoader { -public: - void loadFromRemote( - const std::string& api_url, - const std::string& api_key, - Dispatcher& dispatcher, - std::function callback) { - - // Fetch configuration from remote API - auto http_client = std::make_shared(); - - http_client->request(HttpMethod::GET, api_url, - {{"Authorization", "Bearer " + api_key}}, "", - dispatcher, - [this, &dispatcher, callback](const HttpResponse& response) { - if (response.status_code != 200) { - callback(VoidResult(Error(-1, "Failed to fetch config"))); - return; - } - - // Use ToolsFetcher to build layered architecture - auto fetcher = std::make_shared(); - fetcher->loadFromJson(response.body, dispatcher, - [this, fetcher, callback](VoidResult result) { - if (isSuccess(result)) { - // Store the layers - this->registry_ = fetcher->getRegistry(); - this->composite_ = fetcher->getComposite(); - callback(VoidResult(nullptr)); - } else { - callback(result); - } - }); - }); - } - - std::shared_ptr getRegistry() const { - return registry_; - } - - std::shared_ptr getComposite() const { - return composite_; - } - -private: - std::shared_ptr registry_; // Application layer - std::shared_ptr composite_; // Infrastructure layer -}; -``` - -### Dynamic Tool Reloading with Layered Architecture - -```cpp -class DynamicToolManager { -public: - void reloadTools(Dispatcher& dispatcher) { - // 1. Disconnect all servers in the composite (infrastructure layer) - if (composite_) { - composite_->disconnectAll(dispatcher, [this, &dispatcher]() { - // 2. Clear the composite - composite_->clear(); - - // 3. Clear the registry (application layer) - registry_->clear(); - - // 4. Reload configuration - this->reloadConfig(dispatcher); - }); - } - } - -private: - void reloadConfig(Dispatcher& dispatcher) { - // Fetch new configuration - auto http_client = std::make_shared(); - - http_client->request(HttpMethod::GET, api_url_, - {{"Authorization", "Bearer " + api_key_}}, "", - dispatcher, - [this, &dispatcher](const HttpResponse& response) { - if (response.status_code == 200) { - // Rebuild the layered architecture - auto fetcher = std::make_shared(); - fetcher->loadFromJson(response.body, dispatcher, - [this, fetcher](VoidResult result) { - if (isSuccess(result)) { - // Update both layers - this->composite_ = fetcher->getComposite(); - this->registry_ = fetcher->getRegistry(); - - // Agent automatically uses updated registry - std::cout << "Reloaded " << registry_->toolCount() - << " tools across " << composite_->servers().size() - << " servers\n"; - } - }); - } - }); - } - - std::shared_ptr composite_; // Infrastructure layer - std::shared_ptr registry_; // Application layer - std::shared_ptr agent_; // Agent using registry - std::string api_url_; - std::string api_key_; -}; -``` - -## SDK vs Client Responsibilities - -### SDK Provides - -1. **Configuration Management** - - Parse JSON configurations - - Support multiple formats - - Handle remote API fetching - -2. **Server Management** - - Create MCP server instances - - Manage connections - - Handle reconnection logic - -3. **Tool Discovery** - - Fetch tool definitions - - Cache tool metadata - - Build tool mappings - -4. **Execution Routing** - - Route tool calls to servers - - Handle response transformation - - Manage timeouts and retries - -5. **Adapter Interface** - - ToolRegistry for agents - - ServerComposite for management - - Unified error handling - -### Client Provides - -1. **LLM Provider** - - Choose provider (Anthropic, OpenAI, etc.) - - Configure API keys - - Set model parameters - -2. **Agent Implementation** - - Create agent instance - - Define system prompts - - Set execution parameters - -3. **Business Logic** - - Task definition - - Result processing - - Error handling - -4. **Event Loop** - - Create dispatcher - - Run event processing - - Handle async operations - -## Performance Optimizations - -### Connection Pooling - -```cpp -class ConnectionPool { - std::map> connections_; - - std::shared_ptr getConnection(const std::string& url) { - auto it = connections_.find(url); - if (it != connections_.end() && it->second->isAlive()) { - return it->second; - } - - auto conn = HttpConnection::create(url); - connections_[url] = conn; - return conn; - } -}; -``` - -### Tool Response Caching - -```cpp -class ToolCache { - struct CacheEntry { - JsonValue result; - std::chrono::steady_clock::time_point expiry; - }; - - std::map cache_; - - std::optional get( - const std::string& tool_name, - const JsonValue& arguments) { - auto key = tool_name + ":" + arguments.toString(); - auto it = cache_.find(key); - - if (it != cache_.end()) { - if (std::chrono::steady_clock::now() < it->second.expiry) { - return it->second.result; - } - cache_.erase(it); - } - - return std::nullopt; - } - - void put( - const std::string& tool_name, - const JsonValue& arguments, - const JsonValue& result, - int ttl_seconds = 60) { - auto key = tool_name + ":" + arguments.toString(); - cache_[key] = { - result, - std::chrono::steady_clock::now() + std::chrono::seconds(ttl_seconds) - }; - } -}; -``` - -### Batch Tool Discovery - -```cpp -void ToolsFetcher::discoverToolsParallel( - Dispatcher& dispatcher, - std::function callback) { - - std::atomic pending{static_cast(servers_.size())}; - std::atomic has_error{false}; - - for (auto& server : servers_) { - server->listTools(dispatcher, - [&pending, &has_error, callback](auto result) { - if (isError(result)) { - has_error = true; - } - - if (--pending == 0) { - if (has_error) { - callback(makeError(ErrorCode::DISCOVERY_FAILED)); - } else { - callback(makeSuccess()); - } - } - }); - } -} -``` - -## Error Handling - -### Connection Failures - -```cpp -// MCPServer already has built-in retry logic via config.max_connect_retries -// Example configuration with retries: -MCPServerConfig config; -config.name = "my-server"; -config.transport_type = MCPServerConfig::TransportType::HTTP_SSE; -config.http_sse_transport.url = "http://localhost:3001"; -config.max_connect_retries = 3; -config.retry_delay = std::chrono::milliseconds(1000); - -MCPServer::create(config, dispatcher, [](Result result) { - if (isSuccess(result)) { - // Server connected with automatic retries - } -}); -``` - -### Tool Execution Failures - -```cpp -void handleToolError(const Error& error) { - switch (error.code) { - case ErrorCode::TOOL_NOT_FOUND: - // Tool doesn't exist - may need to refresh - reloadTools(); - break; - - case ErrorCode::SERVER_UNAVAILABLE: - // Server is down - try fallback - useFallbackServer(); - break; - - case ErrorCode::TIMEOUT: - // Request timed out - retry with longer timeout - retryWithTimeout(error.context); - break; - - case ErrorCode::RATE_LIMITED: - // Rate limited - implement backoff - scheduleRetryWithBackoff(error.context); - break; - - default: - // Log and report - logger.error("Tool execution failed", error); - break; - } -} -``` - -## Security Considerations - -### API Key Management - -```cpp -class SecureConfigLoader { - std::string resolveEnvVariables(const std::string& value) { - // Replace ${VAR_NAME} with environment variable - std::regex env_regex("\\$\\{([^}]+)\\}"); - return std::regex_replace(value, env_regex, - [](const std::smatch& match) { - const char* env_val = std::getenv(match[1].str().c_str()); - return env_val ? std::string(env_val) : match[0].str(); - }); - } - - JsonValue sanitizeConfig(const JsonValue& config) { - // Process headers and auth fields - if (config.hasKey("headers")) { - auto headers = config["headers"]; - for (auto& [key, value] : headers.items()) { - headers[key] = resolveEnvVariables(value.getString()); - } - } - return config; - } -}; -``` - -### Tool Filtering - -```cpp -class ToolFilter { - std::vector allowed_patterns_; - std::vector blocked_patterns_; - - bool isAllowed(const std::string& tool_name) const { - // Check blocked list first - for (const auto& pattern : blocked_patterns_) { - if (std::regex_match(tool_name, pattern)) { - return false; - } - } - - // Check allowed list - if (allowed_patterns_.empty()) { - return true; // Allow all if no filters - } - - for (const auto& pattern : allowed_patterns_) { - if (std::regex_match(tool_name, pattern)) { - return true; - } - } - - return false; - } -}; -``` - -## Testing Strategies - -### Mock MCP Server for Testing - -```cpp -class MockMCPServer : public MCPServer { -public: - MockMCPServer() : MCPServer(createMockConfig()) {} - - void listTools( - Dispatcher& dispatcher, - std::function>)> callback) override { - - std::vector tools = { - {"test.echo", "Echo input", createEchoSchema()}, - {"test.delay", "Delay execution", createDelaySchema()} - }; - - dispatcher.post([callback, tools]() { - callback(makeSuccess(tools)); - }); - } - - void callTool( - const std::string& name, - const JsonValue& arguments, - Dispatcher& dispatcher, - JsonCallback callback) override { - - if (name == "test.echo") { - callback(makeSuccess(arguments)); - } else if (name == "test.delay") { - int delay_ms = arguments["delay_ms"].getInt(); - dispatcher.setTimeout(delay_ms, [callback]() { - callback(makeSuccess(JsonValue("delayed"))); - }); - } else { - callback(makeError( - ErrorCode::TOOL_NOT_FOUND, - "Mock tool not found")); - } - } -}; -``` - -### Integration Test - -```cpp -TEST(JsonToAgentPipeline, FullIntegration) { - auto dispatcher = createTestDispatcher(); - - // Create mock configuration - std::string config_json = R"({ - "mcp_servers": [{ - "name": "test-server", - "transport": "http_sse", - "http_sse": { - "url": "http://localhost:9999" - } - }] - })"; - - // Load tools - auto fetcher = std::make_unique(); - fetcher->loadFromJson(config_json, *dispatcher, [](VoidResult result) { - ASSERT_TRUE(isSuccess(result)); - }); - - // Create registry - auto registry = fetcher->createToolRegistry(); - ASSERT_NE(registry, nullptr); - - // Verify tools are available - auto tool_names = registry->getToolNames(); - ASSERT_FALSE(tool_names.empty()); - - // Execute a tool - bool executed = false; - registry->executeTool( - tool_names[0], - JsonValue::object({{"test", "value"}}), - *dispatcher, - [&executed](Result result) { - executed = true; - ASSERT_TRUE(isSuccess(result)); - }); - - // Run dispatcher until complete - while (!executed) { - dispatcher->runOnce(); - } -} -``` - -## Common Patterns - -### Pattern 1: Multi-Environment Configuration - -```cpp -class EnvironmentConfig { - std::map configs_ = { - {"development", "config/dev-tools.json"}, - {"staging", "config/staging-tools.json"}, - {"production", "config/prod-tools.json"} - }; - - std::string getConfigPath() const { - const char* env = std::getenv("ENVIRONMENT"); - std::string environment = env ? env : "development"; - - auto it = configs_.find(environment); - return it != configs_.end() ? it->second : configs_.at("development"); - } -}; -``` - -### Pattern 2: Tool Capability Discovery - -```cpp -class ToolCapabilities { - struct Capability { - bool supports_batch = false; - bool supports_streaming = false; - int max_concurrent = 1; - int timeout_ms = 30000; - }; - - std::map capabilities_; - - void discoverCapabilities( - const ServerToolInfo& tool_info) { - Capability cap; - - if (tool_info.metadata.hasKey("capabilities")) { - auto& meta = tool_info.metadata["capabilities"]; - cap.supports_batch = meta.getValue("batch", false); - cap.supports_streaming = meta.getValue("streaming", false); - cap.max_concurrent = meta.getValue("max_concurrent", 1); - cap.timeout_ms = meta.getValue("timeout_ms", 30000); - } - - capabilities_[tool_info.name] = cap; - } -}; -``` - -### Pattern 3: Health Monitoring - -```cpp -class ServerHealthMonitor { - struct HealthStatus { - bool is_healthy = true; - int consecutive_failures = 0; - std::chrono::steady_clock::time_point last_check; - std::chrono::steady_clock::time_point last_success; - }; - - std::map health_status_; - - void checkHealth( - MCPServerPtr server, - Dispatcher& dispatcher) { - - server->listTools(dispatcher, - [this, server](Result> result) { - auto& status = health_status_[server->name()]; - - if (isSuccess(result)) { - status.is_healthy = true; - status.consecutive_failures = 0; - status.last_success = std::chrono::steady_clock::now(); - } else { - status.consecutive_failures++; - if (status.consecutive_failures >= 3) { - status.is_healthy = false; - onServerUnhealthy(server); - } - } - - status.last_check = std::chrono::steady_clock::now(); - }); - } - - void onServerUnhealthy(MCPServerPtr server) { - // Notify monitoring system - // Remove from rotation - // Attempt reconnection - } -}; -``` - -## Troubleshooting Guide - -### Common Issues and Solutions - -#### 1. Tools Not Appearing in Registry - -**Problem**: Agent can't see tools after loading configuration - -**Solutions**: -- Verify MCP servers are running and accessible -- Check tool discovery responses for errors -- Ensure createAndConnect() completed successfully -- Verify tool names don't have conflicts - -```cpp -// Debug tool loading -auto composite = fetcher->getComposite(); -auto tools = composite->listToolInfos(); -std::cout << "Discovered " << tools.size() << " tools:\n"; -for (const auto& tool : tools) { - std::cout << " - " << tool.name << ": " << tool.description << "\n"; -} -``` - -#### 2. Tool Execution Timeouts - -**Problem**: Tools timeout during execution - -**Solutions**: -- Increase request_timeout_ms in configuration -- Check network latency to MCP servers -- Verify MCP server processing time -- Implement retry logic for transient failures - -```cpp -// Configure longer timeouts -config["mcp_servers"][0]["request_timeout_ms"] = 60000; // 60 seconds -``` - -#### 3. Connection Failures - -**Problem**: Can't connect to MCP servers - -**Solutions**: -- Verify server URLs are correct -- Check firewall rules -- Ensure authentication headers are set -- Test with curl directly - -```bash -# Test MCP server connectivity -curl -X POST http://localhost:3001/tools \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"test"}' -``` - -#### 4. Memory Leaks - -**Problem**: Memory usage grows over time - -**Solutions**: -- Ensure proper cleanup of server connections -- Clear tool cache periodically -- Use weak_ptr for circular references -- Profile with valgrind or sanitizers - -```cpp -// Periodic cleanup -void cleanupResources() { - // Clear cache - tool_cache_.clear(); - - // Disconnect unused servers - for (auto& server : idle_servers_) { - server->disconnect(dispatcher_, [](){}); - } - idle_servers_.clear(); -} -``` - -## Future Enhancements - -### Planned Features - -1. **WebSocket Support** - - Real-time tool updates - - Bidirectional communication - - Lower latency execution - -2. **GraphQL Integration** - - Query-based tool discovery - - Selective field fetching - - Subscription support - -3. **Tool Versioning** - - Version compatibility checking - - Automatic migration - - Deprecation warnings - -4. **Distributed Tracing** - - OpenTelemetry integration - - Request correlation - - Performance profiling - -5. **Advanced Caching** - - Redis integration - - Distributed cache - - Cache invalidation protocols - -## Conclusion - -The JSON-to-Agent Pipeline implements a **layered architecture** that leverages **95% existing gopher-orch infrastructure**, requiring minimal new code. - -### Layered Architecture Benefits - -#### 1. Clean Separation of Concerns -- **Infrastructure Layer (ServerComposite)**: Manages server connections, pooling, and routing -- **Application Layer (ToolRegistry)**: Provides agent-friendly interface with tool specs -- **Clear Boundaries**: Each layer has distinct responsibilities - -#### 2. Scalability and Performance -- **Single Composite Backend**: All servers managed by one efficient composite -- **Namespace Management**: Tools properly namespaced to avoid conflicts -- **Connection Pooling**: Reused connections across multiple tool calls -- **Caching**: Tool metadata cached at both layers - -#### 3. Flexibility -- **Mix Local and Remote Tools**: ToolRegistry handles both seamlessly -- **Dynamic Server Addition**: Add/remove servers without affecting agents -- **Multiple Discovery Sources**: Support for MCP, REST, and local tools - -### Existing Components Used: -1. **ServerComposite** - Infrastructure layer for multi-server management -2. **ToolRegistry** - Application layer for agent integration -3. **MCPServer** - Complete MCP protocol implementation -4. **ConfigLoader** - JSON parsing with environment variables -5. **HttpClient** - HTTP communication for remote APIs -6. **ToolExecutor** - Execution delegation from registry to servers - -### Minimal New Code Required: -- **ToolsFetcher** - ~100 line orchestration layer that coordinates the architecture - -### Key Implementation Insights: - -1. **ToolRegistry and ServerComposite are Complementary** - - Not competing solutions but layers in a larger architecture - - ToolRegistry uses ServerComposite as its backend for server tools - - Clean delegation pattern maintains separation - -2. **Unified Tool Interface** - - Agents only interact with ToolRegistry - - Registry handles routing to local functions or ServerComposite - - ServerComposite manages the complexity of multiple servers - -3. **Production Ready** - - Inherits robust error handling from existing components - - Built-in connection management and retries - - Monitoring and health checks at each layer - -This layered approach demonstrates that gopher-orch provides a complete, production-ready JSON-to-Agent pipeline. The combination of `ToolRegistry` (application layer) + `ServerComposite` (infrastructure layer) + `MCPServer` (protocol layer) creates a powerful, flexible, and maintainable architecture for tool management. - -## Appendix: Complete Example - -See `examples/sdk/sdk_example.cpp` for a complete working implementation of the JSON-to-Agent pipeline, demonstrating: -- Configuration loading -- Tool discovery -- Agent creation -- Task execution -- Error handling -- Resource cleanup - -## References - -- [MCP Protocol Specification](https://github.com/modelcontextprotocol/specification) -- [Runnable Architecture](./Runnable.md) -- [Agent Framework](./Agent.md) -- [ServerComposite Pattern](./Server.md) -- [SDK Examples](../examples/sdk/) \ No newline at end of file diff --git a/third_party/gopher-orch/docs/LLMProvider.md b/third_party/gopher-orch/docs/LLMProvider.md deleted file mode 100644 index 283237a3..00000000 --- a/third_party/gopher-orch/docs/LLMProvider.md +++ /dev/null @@ -1,331 +0,0 @@ -# LLMProvider Design Document - -## Overview - -LLMProvider is an abstract interface that provides a unified way to interact with various Large Language Model providers (OpenAI, Anthropic, Ollama, etc.). It handles the complexities of different API formats while exposing a consistent async interface for chat completions with tool support. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Application │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ LLMProvider (Abstract) │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ • chat(messages, tools, config, dispatcher, callback) │ │ -│ │ • chatStream(messages, tools, config, ...) │ │ -│ │ • isModelSupported(model) │ │ -│ │ • supportedModels() │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ OpenAIProvider │ │AnthropicProvider│ │ OllamaProvider │ -│ │ │ │ │ │ -│ • GPT-4 │ │ • Claude 3 │ │ • Llama 2 │ -│ • GPT-3.5 │ │ • Claude 3.5 │ │ • Mistral │ -│ • GPT-4o │ │ • Claude Opus │ │ • Custom │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ HttpClient │ -│ (Async HTTP requests via Dispatcher) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Core Components - -### 1. Message Types - -```cpp -enum class Role { - SYSTEM, // System prompt - USER, // User message - ASSISTANT, // Assistant response - TOOL // Tool result -}; - -struct Message { - Role role; - std::string content; - optional tool_call_id; // For TOOL role - optional> tool_calls; // For ASSISTANT with tools -}; -``` - -### 2. Tool Specification - -```cpp -struct ToolSpec { - std::string name; - std::string description; - JsonValue parameters; // JSON Schema -}; - -struct ToolCall { - std::string id; // Unique ID for matching results - std::string name; // Tool name - JsonValue arguments; // Arguments from LLM -}; -``` - -### 3. LLM Configuration - -```cpp -struct LLMConfig { - std::string model; // e.g., "gpt-4", "claude-3-opus" - optional temperature; // 0.0 - 2.0 - optional max_tokens; // Max response tokens - optional top_p; // Nucleus sampling - optional seed; // For reproducibility - std::chrono::milliseconds timeout{60000}; -}; -``` - -## Request Flow - -``` -┌──────────┐ ┌────────────┐ ┌──────────────┐ ┌─────────┐ -│ Client │────▶│ LLMProvider│────▶│ HttpClient │────▶│ LLM API │ -└──────────┘ └────────────┘ └──────────────┘ └─────────┘ - │ │ │ │ - │ chat() │ │ │ - │────────────────▶│ │ │ - │ │ buildRequest() │ │ - │ │──────────────────▶│ │ - │ │ │ HTTP POST │ - │ │ │──────────────────▶│ - │ │ │ │ - │ │ │◀──────────────────│ - │ │ │ JSON Response │ - │ │◀──────────────────│ │ - │ │ parseResponse() │ │ - │◀────────────────│ │ │ - │ callback() │ │ │ - │ LLMResponse │ │ │ -``` - -## Provider-Specific Message Conversion - -### OpenAI Format - -```json -{ - "model": "gpt-4", - "messages": [ - {"role": "system", "content": "..."}, - {"role": "user", "content": "..."}, - {"role": "assistant", "content": "...", "tool_calls": [...]}, - {"role": "tool", "tool_call_id": "...", "content": "..."} - ], - "tools": [...] -} -``` - -### Anthropic Format - -```json -{ - "model": "claude-3-opus-20240229", - "system": "...", - "messages": [ - {"role": "user", "content": "..."}, - {"role": "assistant", "content": [ - {"type": "text", "text": "..."}, - {"type": "tool_use", "id": "...", "name": "...", "input": {...}} - ]}, - {"role": "user", "content": [ - {"type": "tool_result", "tool_use_id": "...", "content": "..."} - ]} - ], - "tools": [...] -} -``` - -## Example Usage - -### Basic Chat - -```cpp -#include "gopher/orch/llm/openai_provider.h" - -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -// Create provider -auto provider = OpenAIProvider::create("sk-your-api-key"); - -// Configure request -LLMConfig config("gpt-4"); -config.withTemperature(0.7).withMaxTokens(1000); - -// Build messages -std::vector messages = { - Message::system("You are a helpful assistant."), - Message::user("What is the capital of France?") -}; - -// Make async request -provider->chat(messages, {}, config, dispatcher, - [](Result result) { - if (mcp::holds_alternative(result)) { - auto& response = mcp::get(result); - std::cout << "Response: " << response.message.content << std::endl; - std::cout << "Tokens used: " << response.usage->total_tokens << std::endl; - } else { - auto& error = mcp::get(result); - std::cerr << "Error: " << error.message << std::endl; - } - }); -``` - -### Chat with Tools - -```cpp -// Define tools -std::vector tools; - -JsonValue weatherParams = JsonValue::object(); -weatherParams["type"] = "object"; -JsonValue props = JsonValue::object(); -JsonValue locationProp = JsonValue::object(); -locationProp["type"] = "string"; -locationProp["description"] = "City name"; -props["location"] = locationProp; -weatherParams["properties"] = props; -weatherParams["required"] = JsonValue::array(); -weatherParams["required"].push_back("location"); - -tools.push_back(ToolSpec("get_weather", "Get current weather", weatherParams)); - -// Chat with tools -provider->chat(messages, tools, config, dispatcher, - [](Result result) { - if (mcp::holds_alternative(result)) { - auto& response = mcp::get(result); - - if (response.hasToolCalls()) { - // LLM wants to call tools - for (const auto& call : response.toolCalls()) { - std::cout << "Tool call: " << call.name << std::endl; - std::cout << "Arguments: " << call.arguments.toString() << std::endl; - } - } else { - // Final response - std::cout << "Response: " << response.message.content << std::endl; - } - } - }); -``` - -### Using Anthropic Provider - -```cpp -#include "gopher/orch/llm/anthropic_provider.h" - -// Create with custom configuration -AnthropicConfig config("your-api-key"); -config.withBaseUrl("https://api.anthropic.com") - .withApiVersion("2023-06-01") - .withBeta("tools-2024-04-04"); - -auto provider = AnthropicProvider::create(config); - -// Use same interface as OpenAI -LLMConfig llmConfig("claude-3-5-sonnet-latest"); -provider->chat(messages, tools, llmConfig, dispatcher, callback); -``` - -### Using Factory - -```cpp -#include "gopher/orch/llm/llm_provider.h" - -// Create via factory -ProviderConfig config(ProviderType::OPENAI); -config.withApiKey("sk-...") - .withBaseUrl("https://custom-endpoint.com"); - -auto provider = createProvider(config); - -// Or use convenience functions -auto openai = createOpenAIProvider("sk-..."); -auto anthropic = createAnthropicProvider("ant-..."); -auto ollama = createOllamaProvider("http://localhost:11434"); -``` - -## Error Handling - -```cpp -namespace LLMError { - enum : int { - OK = 0, - INVALID_API_KEY = -100, - RATE_LIMITED = -101, - CONTEXT_LENGTH_EXCEEDED = -102, - INVALID_MODEL = -103, - CONTENT_FILTERED = -104, - SERVICE_UNAVAILABLE = -105, - NETWORK_ERROR = -106, - PARSE_ERROR = -107, - UNKNOWN = -199 - }; -} - -// Handle errors -provider->chat(messages, tools, config, dispatcher, - [](Result result) { - if (!mcp::holds_alternative(result)) { - auto& error = mcp::get(result); - switch (error.code) { - case LLMError::RATE_LIMITED: - // Implement retry with backoff - break; - case LLMError::INVALID_API_KEY: - // Check API key configuration - break; - case LLMError::CONTEXT_LENGTH_EXCEEDED: - // Reduce message history - break; - } - } - }); -``` - -## Thread Safety - -- All public methods must be called from the dispatcher thread -- Callbacks are invoked in the dispatcher thread context -- Provider instances can be shared across multiple calls -- Configuration should be done before making requests - -## Extensibility - -To add a new provider: - -1. Create header `include/gopher/orch/llm/new_provider.h` -2. Implement `LLMProvider` interface -3. Handle provider-specific message/tool format conversion -4. Add factory function to `llm_provider.h` - -```cpp -class NewProvider : public LLMProvider { - public: - std::string name() const override { return "new-provider"; } - - void chat(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) override { - // Implementation - } - - // ... other methods -}; -``` diff --git a/third_party/gopher-orch/docs/MCP-Gateway-Config-Reference.md b/third_party/gopher-orch/docs/MCP-Gateway-Config-Reference.md deleted file mode 100644 index f3e0f7fa..00000000 --- a/third_party/gopher-orch/docs/MCP-Gateway-Config-Reference.md +++ /dev/null @@ -1,275 +0,0 @@ -# MCP Gateway Configuration Reference - -Quick reference for backend developers on MCP Gateway environment variables and configuration formats. - ---- - -## Environment Variables - -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `MCP_GATEWAY_CONFIG_PATH` | Yes | `/etc/mcp/gateway-config.json` | Path to config file (manifest from pod secret) | -| `MCP_GATEWAY_CONFIG_URL` | No | - | API URL to fetch config (fallback mechanism) | -| `MCP_GATEWAY_PORT` | Yes | `3003` | Server listen port | -| `MCP_GATEWAY_HOST` | Yes | `0.0.0.0` | Server listen host | -| `MCP_GATEWAY_NAME` | Yes | `mcp-gateway` | Server name for logging | - -**Example API URL:** -``` -https://api-test.gopher.security/v1/mcp-gateway/{gatewayId}/manifest?accessKeyId={accessKeyId} -``` - ---- - -## Configuration JSON Formats - -### Gateway Manifest (Pod Secret) - -Primary configuration from `MCP_GATEWAY_CONFIG_PATH`: - -```json -{ - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01", - "generatedAt": 1768114552523 - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2, - "retryableCodes": [429, 500, 502, 503, 504] - } - }, - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "gopher-auth-server", - "url": "http://127.0.0.1:3001/mcp" - }, - { - "version": "2025-01-09", - "serverId": "1877234567890123457", - "name": "gopher-auth-server2", - "url": "http://127.0.0.1:3002/mcp" - } - ] -} -``` - -### API Response Format (Fallback) - -Returned by `MCP_GATEWAY_CONFIG_URL` endpoint: - -```json -{ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01", - "generatedAt": 1768114552523 - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2, - "retryableCodes": [429, 500, 502, 503, 504] - } - }, - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "gopher-auth-server", - "url": "http://127.0.0.1:3001/mcp" - }, - { - "version": "2025-01-09", - "serverId": "1877234567890123457", - "name": "gopher-auth-server2", - "url": "http://127.0.0.1:3002/mcp" - } - ] - } -} -``` - ---- - -## Schema Reference - -### Root Manifest Object - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `version` | string | Yes | Manifest schema version (e.g., "2026-01-11") | -| `metadata` | object | Yes | Gateway metadata | -| `config` | object | Yes | Gateway configuration | -| `servers` | array | Yes | List of backend MCP servers | - -### Metadata Object - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `accountId` | string | Yes | Account identifier | -| `gatewayId` | string | Yes | Gateway identifier | -| `gatewayName` | string | Yes | Gateway display name | -| `generatedAt` | number | Yes | Timestamp when manifest was generated (epoch ms) | - -### Config Object - -| Field | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `connectTimeout` | number | No | 5000 | Connection timeout in milliseconds | -| `requestTimeout` | number | No | 30000 | Request timeout in milliseconds | -| `retryPolicy` | object | No | - | Retry configuration | - -### Retry Policy Object - -| Field | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `maxAttempts` | number | No | 5 | Maximum retry attempts | -| `initialBackoff` | number | No | 1000 | Initial backoff in milliseconds | -| `backoffMultiplier` | number | No | 2.0 | Backoff multiplier | -| `maxBackoff` | number | No | 30000 | Maximum backoff in milliseconds | -| `jitter` | number | No | 0.2 | Jitter factor (0.0-1.0) | -| `retryableCodes` | array | No | [429,500,502,503,504] | HTTP codes to retry | - -### Server Object - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `serverId` | string | Yes | Unique server identifier | -| `version` | string | Yes | Server configuration version | -| `name` | string | Yes | Server display name | -| `url` | string | Yes | Backend MCP server endpoint URL | - -### API Response Wrapper - -| Field | Type | Description | -|-------|------|-------------| -| `succeeded` | boolean | `true` if request successful | -| `code` | number | Response code (e.g., 200000000) | -| `message` | string | Response message (e.g., "success") | -| `data` | object | Gateway manifest | - ---- - -## API Endpoint - -### Manifest Fetch URL - -``` -GET https://api-test.gopher.security/v1/mcp-gateway/{gatewayId}/manifest?accessKeyId={accessKeyId} -``` - -| Parameter | Location | Description | -|-----------|----------|-------------| -| `gatewayId` | Path | Gateway identifier | -| `accessKeyId` | Query | Access key for authentication | - -### Error Response - -```json -{ - "succeeded": false, - "code": 400000001, - "message": "Gateway not found", - "data": null -} -``` - ---- - -## Configuration Priority - -1. `MCP_GATEWAY_CONFIG_PATH` (pod secret file) - **Primary** -2. `MCP_GATEWAY_CONFIG_URL` (API endpoint) - **Fallback** - -The gateway loads configuration from the file first. If the file doesn't exist or is invalid, it falls back to the API endpoint. - ---- - -## Example Kubernetes Secret - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: mcp-gateway-config - namespace: mcp-system -type: Opaque -stringData: - gateway-config.json: | - { - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01", - "generatedAt": 1768114552523 - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2, - "retryableCodes": [429, 500, 502, 503, 504] - } - }, - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "weather-service", - "url": "http://weather-service.mcp-system.svc.cluster.local:3001/mcp" - }, - { - "version": "2025-01-09", - "serverId": "1877234567890123457", - "name": "auth-service", - "url": "http://auth-service.mcp-system.svc.cluster.local:3002/mcp" - } - ] - } -``` - ---- - -## Example Deployment Environment - -```yaml -env: -- name: MCP_GATEWAY_CONFIG_PATH - value: "/etc/mcp/gateway-config.json" -- name: MCP_GATEWAY_CONFIG_URL - value: "https://api-test.gopher.security/v1/mcp-gateway/694821867856330753/manifest?accessKeyId=AK7HZ9N61Z0B59SYCBD5" -- name: MCP_GATEWAY_PORT - value: "3003" -- name: MCP_GATEWAY_HOST - value: "0.0.0.0" -- name: MCP_GATEWAY_NAME - value: "mcp-gateway" -``` diff --git a/third_party/gopher-orch/docs/MCP-Gateway-Deployment.md b/third_party/gopher-orch/docs/MCP-Gateway-Deployment.md deleted file mode 100644 index 6dd8c608..00000000 --- a/third_party/gopher-orch/docs/MCP-Gateway-Deployment.md +++ /dev/null @@ -1,905 +0,0 @@ -# MCP Gateway Deployment Guide - -## Overview - -This guide covers deploying the **MCP Gateway Server** as a containerized service in Kubernetes. The gateway aggregates tools from multiple backend MCP servers and exposes them through a single unified endpoint. - -### Deployment Architecture - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Kubernetes Cluster │ -│ │ -│ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ MCP Gateway Pod │ │ -│ │ │ │ -│ │ ┌─────────────────┐ ┌─────────────────────────────────────┐ │ │ -│ │ │ Secret │ │ mcp-gateway container │ │ │ -│ │ │ (Config) │───▶│ │ │ │ -│ │ │ │ │ - Loads config from env/file/API │ │ │ -│ │ └─────────────────┘ │ - Connects to backend MCP servers │ │ │ -│ │ │ - Exposes unified tool endpoint │ │ │ -│ │ │ - Port 3003 (configurable) │ │ │ -│ │ └─────────────────────────────────────┘ │ │ -│ │ │ │ │ -│ └────────────────────────────────────────┼───────────────────────────┘ │ -│ │ │ -│ ┌────────────────────────────────────────┼───────────────────────────┐ │ -│ │ Service (LoadBalancer/ClusterIP) │ │ -│ │ Port 3003 │ │ -│ └────────────────────────────────────────┼───────────────────────────┘ │ -│ │ │ -└───────────────────────────────────────────┼──────────────────────────────┘ - │ - ▼ - External MCP Clients - (Claude Desktop, ReActAgent, etc.) -``` - ---- - -## Quick Start - -### 1. Build and Push Docker Image - -```bash -# Clone the repository -git clone https://github.com/your-org/gopher-orch.git -cd gopher-orch - -# Build and push to ECR -./docker/build-and-push.sh -``` - -### 2. Create Kubernetes Secret - -```bash -# Create secret with gateway configuration -kubectl create secret generic mcp-gateway-config \ - --from-file=gateway-config.json=/path/to/your/config.json -``` - -### 3. Deploy to Kubernetes - -```bash -kubectl apply -f k8s/mcp-gateway-deployment.yaml -``` - ---- - -## Building the Docker Image - -### Prerequisites - -- Docker with Buildx support -- AWS CLI configured with ECR access -- Git (for submodule initialization) - -### Build Script - -The `docker/build-and-push.sh` script handles the complete build and push workflow: - -```bash -# Default configuration -./docker/build-and-push.sh - -# Custom configuration -AWS_REGION=us-west-2 \ -AWS_ACCOUNT_ID=123456789012 \ -REPOSITORY_NAME=my-mcp-gateway \ -./docker/build-and-push.sh -``` - -### Build Script Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `AWS_REGION` | `us-east-1` | AWS region for ECR | -| `AWS_ACCOUNT_ID` | (required) | Your AWS account ID | -| `REPOSITORY_NAME` | `mcp-gateway` | ECR repository name | - -### Manual Build - -```bash -# Build for local testing (current architecture only) -docker build -t mcp-gateway -f docker/Dockerfile . - -# Build multi-arch and push to registry -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - -t your-registry/mcp-gateway:latest \ - -f docker/Dockerfile \ - --push \ - . -``` - -### Image Tags - -The build script creates the following tags: - -| Tag | Description | -|-----|-------------| -| `latest` | Most recent build | -| `amd64` | AMD64 architecture | -| `arm64` | ARM64 architecture | -| `YYYY.MM.DD-HHMM` | Timestamped version | - ---- - -## Configuration - -The MCP Gateway supports three configuration sources, in priority order: - -1. **Environment Variable** (`MCP_GATEWAY_CONFIG`) - JSON string -2. **Config File** (`MCP_GATEWAY_CONFIG_PATH`) - File path, typically mounted from Kubernetes Secret -3. **API Endpoint** (`MCP_GATEWAY_CONFIG_URL`) - Fetches config from remote API - -### Environment Variables Reference - -#### Required (at least one config source) - -| Variable | Description | -|----------|-------------| -| `MCP_GATEWAY_CONFIG` | JSON configuration string (highest priority) | -| `MCP_GATEWAY_CONFIG_PATH` | Path to config file (default: `/etc/mcp/gateway-config.json`) | -| `MCP_GATEWAY_CONFIG_URL` | API URL to fetch configuration | - -#### Optional - -| Variable | Default | Description | -|----------|---------|-------------| -| `MCP_GATEWAY_ACCESS_KEY` | - | Access key for API authentication | -| `MCP_GATEWAY_PORT` | `3003` | Server listen port | -| `MCP_GATEWAY_HOST` | `0.0.0.0` | Server listen host | -| `MCP_GATEWAY_NAME` | `mcp-gateway` | Server name for logging | - -### Configuration JSON Formats - -The gateway supports two JSON formats depending on the source: - -#### Manifest Format (Config File or Environment Variable) - -Config files use the full manifest format with metadata, config, and servers: - -```json -{ - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01", - "generatedAt": 1768114552523 - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2, - "retryableCodes": [429, 500, 502, 503, 504] - } - }, - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "weather-service", - "url": "http://weather-service.default.svc.cluster.local:3001/mcp" - }, - { - "version": "2025-01-09", - "serverId": "1877234567890123457", - "name": "auth-service", - "url": "http://auth-service.default.svc.cluster.local:3002/mcp" - } - ] -} -``` - -#### API Response Format (MCP_GATEWAY_CONFIG_URL) - -API endpoints return wrapped format with `succeeded` and `data`: - -```json -{ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01", - "generatedAt": 1768114552523 - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2, - "retryableCodes": [429, 500, 502, 503, 504] - } - }, - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "weather-service", - "url": "http://weather-service.mcp-system.svc.cluster.local:3001/mcp" - } - ] - } -} -``` - -### Server Object Fields - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `serverId` | string | Yes | Unique server identifier | -| `version` | string | Yes | Server configuration version | -| `name` | string | Yes | Server display name | -| `url` | string | Yes* | HTTP/SSE endpoint URL | -| `command` | string | Yes* | Stdio command (alternative to url) | -| `args` | array | No | Command arguments (for stdio) | - -*Either `url` (HTTP/SSE) or `command` (stdio) is required. - -### Transport Auto-Detection - -The gateway automatically detects the transport type: -- **HTTP/SSE**: Server has `url` field -- **Stdio**: Server has `command` field - ---- - -## Kubernetes Deployment - -### Complete Deployment Manifest - -Create `k8s/mcp-gateway-deployment.yaml`: - -```yaml ---- -# Namespace (optional) -apiVersion: v1 -kind: Namespace -metadata: - name: mcp-system - ---- -# Secret containing gateway configuration -apiVersion: v1 -kind: Secret -metadata: - name: mcp-gateway-config - namespace: mcp-system -type: Opaque -stringData: - gateway-config.json: | - { - "version": "2026-01-11", - "metadata": { - "accountId": "348716338765762562", - "gatewayId": "694821867856330753", - "gatewayName": "mcp-toolkit-01", - "generatedAt": 1768114552523 - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000, - "retryPolicy": { - "maxAttempts": 5, - "initialBackoff": 1000, - "backoffMultiplier": 2.0, - "maxBackoff": 30000, - "jitter": 0.2, - "retryableCodes": [429, 500, 502, 503, 504] - } - }, - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "weather-service", - "url": "http://weather-service.mcp-system.svc.cluster.local:3001/mcp" - }, - { - "version": "2025-01-09", - "serverId": "1877234567890123457", - "name": "auth-service", - "url": "http://auth-service.mcp-system.svc.cluster.local:3002/mcp" - } - ] - } - ---- -# Deployment -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mcp-gateway - namespace: mcp-system - labels: - app: mcp-gateway -spec: - replicas: 2 - selector: - matchLabels: - app: mcp-gateway - template: - metadata: - labels: - app: mcp-gateway - spec: - containers: - - name: mcp-gateway - image: 745308818994.dkr.ecr.us-east-1.amazonaws.com/mcp-gateway:latest - ports: - - containerPort: 3003 - name: http - env: - - name: MCP_GATEWAY_PORT - value: "3003" - - name: MCP_GATEWAY_HOST - value: "0.0.0.0" - - name: MCP_GATEWAY_NAME - value: "mcp-gateway" - - name: MCP_GATEWAY_CONFIG_PATH - value: "/etc/mcp/gateway-config.json" - volumeMounts: - - name: config - mountPath: /etc/mcp - readOnly: true - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" - livenessProbe: - httpGet: - path: /health - port: 3003 - initialDelaySeconds: 10 - periodSeconds: 30 - timeoutSeconds: 5 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /health - port: 3003 - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 3 - volumes: - - name: config - secret: - secretName: mcp-gateway-config - securityContext: - runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 1001 - fsGroup: 1001 - ---- -# Service -apiVersion: v1 -kind: Service -metadata: - name: mcp-gateway - namespace: mcp-system - labels: - app: mcp-gateway -spec: - type: ClusterIP - ports: - - port: 3003 - targetPort: 3003 - protocol: TCP - name: http - selector: - app: mcp-gateway - ---- -# Horizontal Pod Autoscaler (optional) -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: mcp-gateway - namespace: mcp-system -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: mcp-gateway - minReplicas: 2 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 -``` - -### Deploy Commands - -```bash -# Apply all resources -kubectl apply -f k8s/mcp-gateway-deployment.yaml - -# Check deployment status -kubectl -n mcp-system get pods -l app=mcp-gateway - -# View logs -kubectl -n mcp-system logs -l app=mcp-gateway -f - -# Check service -kubectl -n mcp-system get svc mcp-gateway -``` - ---- - -## Configuration Methods - -### Method 1: Kubernetes Secret (Recommended for Production) - -```bash -# Create secret from file -kubectl -n mcp-system create secret generic mcp-gateway-config \ - --from-file=gateway-config.json=./config/gateway-config.json - -# Or create secret from literal JSON -kubectl -n mcp-system create secret generic mcp-gateway-config \ - --from-literal=gateway-config.json='{"succeeded":true,"data":{"servers":[...]}}' -``` - -Mount in deployment: -```yaml -volumes: -- name: config - secret: - secretName: mcp-gateway-config -``` - -### Method 2: Environment Variable (Simple Deployments) - -```yaml -env: -- name: MCP_GATEWAY_CONFIG - value: '{"version":"2026-01-11","metadata":{"gatewayId":"123"},"config":{"connectTimeout":5000,"requestTimeout":30000},"servers":[{"serverId":"1","name":"server1","url":"http://localhost:3001/mcp"}]}' -``` - -### Method 3: ConfigMap (Non-Sensitive Configuration) - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: mcp-gateway-config - namespace: mcp-system -data: - gateway-config.json: | - { - "version": "2026-01-11", - "metadata": { - "gatewayId": "694821867856330753", - "gatewayName": "mcp-gateway-dev" - }, - "config": { - "connectTimeout": 5000, - "requestTimeout": 30000 - }, - "servers": [ - { - "serverId": "1877234567890123456", - "name": "public-service", - "url": "http://public-service:3001/mcp" - } - ] - } -``` - -### Method 4: API Endpoint (Dynamic Configuration) - -```yaml -env: -- name: MCP_GATEWAY_CONFIG_URL - value: "https://api.example.com/v1/mcp-servers/12345/manifest" -- name: MCP_GATEWAY_ACCESS_KEY - valueFrom: - secretKeyRef: - name: api-credentials - key: access-key -``` - ---- - -## Health Checks and Monitoring - -### Health Endpoint - -The gateway exposes a health check endpoint at `/health`: - -```bash -# Check health status -curl http://mcp-gateway:3003/health -``` - -Response: -```json -{ - "status": "healthy", - "uptime_seconds": 3600, - "tool_count": 42, - "server_count": 3 -} -``` - -### Kubernetes Probes - -```yaml -livenessProbe: - httpGet: - path: /health - port: 3003 - initialDelaySeconds: 10 - periodSeconds: 30 - timeoutSeconds: 5 - failureThreshold: 3 - -readinessProbe: - httpGet: - path: /health - port: 3003 - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 3 -``` - -### Monitoring with Prometheus - -```yaml -# ServiceMonitor for Prometheus Operator -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: mcp-gateway - namespace: mcp-system -spec: - selector: - matchLabels: - app: mcp-gateway - endpoints: - - port: http - path: /metrics - interval: 30s -``` - -### Logging - -View gateway logs: - -```bash -# Follow logs -kubectl -n mcp-system logs -l app=mcp-gateway -f - -# Get logs from specific pod -kubectl -n mcp-system logs mcp-gateway-xxxxxx-xxxxx - -# Get logs with timestamps -kubectl -n mcp-system logs -l app=mcp-gateway --timestamps -``` - -Log output format: -``` -[Gateway] MCP Gateway Server starting... -[Gateway] Loading config from /etc/mcp/gateway-config.json -[Gateway] Creating gateway server... -[Gateway] Gateway created successfully -[Gateway] Tools: 15 -[Gateway] Servers: 3 -[Gateway] Listening on 0.0.0.0:3003 -``` - ---- - -## Production Best Practices - -### 1. Resource Limits - -Always set resource requests and limits: - -```yaml -resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" -``` - -### 2. Security Context - -Run as non-root user: - -```yaml -securityContext: - runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 1001 - fsGroup: 1001 - readOnlyRootFilesystem: true -``` - -### 3. Network Policies - -Restrict network access: - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: mcp-gateway - namespace: mcp-system -spec: - podSelector: - matchLabels: - app: mcp-gateway - policyTypes: - - Ingress - - Egress - ingress: - - from: - - namespaceSelector: - matchLabels: - name: client-namespace - ports: - - port: 3003 - egress: - - to: - - namespaceSelector: - matchLabels: - name: mcp-system - ports: - - port: 3001 - - port: 3002 -``` - -### 4. Pod Disruption Budget - -Ensure availability during updates: - -```yaml -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: mcp-gateway - namespace: mcp-system -spec: - minAvailable: 1 - selector: - matchLabels: - app: mcp-gateway -``` - -### 5. Rolling Updates - -Configure update strategy: - -```yaml -spec: - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 -``` - -### 6. Anti-Affinity - -Spread pods across nodes: - -```yaml -affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchLabels: - app: mcp-gateway - topologyKey: kubernetes.io/hostname -``` - ---- - -## Updating the Gateway - -### Rolling Update with New Image - -```bash -# Push new image -./docker/build-and-push.sh - -# Update deployment to use new image -kubectl -n mcp-system set image deployment/mcp-gateway \ - mcp-gateway=745308818994.dkr.ecr.us-east-1.amazonaws.com/mcp-gateway:2024.01.15-1430 - -# Or use latest and restart pods -kubectl -n mcp-system rollout restart deployment/mcp-gateway - -# Watch rollout status -kubectl -n mcp-system rollout status deployment/mcp-gateway -``` - -### Updating Configuration - -```bash -# Update secret -kubectl -n mcp-system create secret generic mcp-gateway-config \ - --from-file=gateway-config.json=./config/new-config.json \ - --dry-run=client -o yaml | kubectl apply -f - - -# Restart pods to pick up new config -kubectl -n mcp-system rollout restart deployment/mcp-gateway -``` - -### Rollback - -```bash -# View rollout history -kubectl -n mcp-system rollout history deployment/mcp-gateway - -# Rollback to previous version -kubectl -n mcp-system rollout undo deployment/mcp-gateway - -# Rollback to specific revision -kubectl -n mcp-system rollout undo deployment/mcp-gateway --to-revision=2 -``` - ---- - -## Troubleshooting - -### Gateway Won't Start - -**Symptom:** Pod in CrashLoopBackOff - -**Check logs:** -```bash -kubectl -n mcp-system logs mcp-gateway-xxxxx -``` - -**Common causes:** -1. Missing configuration - Ensure secret is mounted -2. Invalid JSON - Validate configuration format -3. Permission denied - Check security context - -### No Tools Registered - -**Symptom:** `toolCount() == 0` - -**Solutions:** -1. Check backend server URLs are accessible from gateway pod -2. Verify backend servers are running -3. Check network policies allow egress to backends -4. Enable debug logging: - ```yaml - env: - - name: GOPHER_LOG_LEVEL - value: "debug" - ``` - -### Connection Refused to Backends - -**Symptom:** Gateway can't reach backend services - -**Debug:** -```bash -# Exec into pod -kubectl -n mcp-system exec -it mcp-gateway-xxxxx -- /bin/sh - -# Test connectivity (if curl available in image) -curl -v http://weather-service:3001/mcp -``` - -**Solutions:** -1. Verify service DNS names are correct -2. Check services are running: `kubectl get svc` -3. Verify network policies - -### Health Check Failing - -**Symptom:** Pod not becoming Ready - -**Check:** -```bash -# Test health endpoint manually -kubectl -n mcp-system port-forward svc/mcp-gateway 3003:3003 -curl localhost:3003/health -``` - -**Solutions:** -1. Increase `initialDelaySeconds` if gateway takes time to start -2. Check gateway logs for startup errors -3. Verify port configuration matches probe port - ---- - -## Local Testing - -### Run Locally with Docker - -```bash -# Build image -docker build -t mcp-gateway -f docker/Dockerfile . - -# Run with environment variable config -docker run -p 3003:3003 \ - -e MCP_GATEWAY_CONFIG='{"version":"2026-01-11","metadata":{"gatewayId":"123"},"config":{},"servers":[{"serverId":"1","name":"test","url":"http://host.docker.internal:3001/mcp"}]}' \ - mcp-gateway - -# Run with mounted config file -docker run -p 3003:3003 \ - -v $(pwd)/config:/etc/mcp:ro \ - mcp-gateway -``` - -### Test Health Endpoint - -```bash -curl http://localhost:3003/health -``` - -### Test Tool Listing - -```bash -# Using curl to test MCP endpoint -curl -X POST http://localhost:3003/mcp \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' -``` - ---- - -## See Also - -- [GatewayServer API Documentation](GatewayServer.md) - Detailed API reference -- [MCP Protocol Specification](https://spec.modelcontextprotocol.io/) - Official MCP protocol docs -- [Docker Documentation](../docker/README.md) - Docker build details - ---- - -## Changelog - -### Version 1.0.0 (2026-01-14) - -**Initial Release:** -- Multi-stage Docker build (Ubuntu 22.04) -- Multi-architecture support (amd64, arm64) -- Three configuration methods (env, file, API) -- Production-ready Kubernetes manifests -- Health check endpoint -- Non-root container execution -- Comprehensive deployment documentation diff --git a/third_party/gopher-orch/docs/Resilience.md b/third_party/gopher-orch/docs/Resilience.md deleted file mode 100644 index 385076de..00000000 --- a/third_party/gopher-orch/docs/Resilience.md +++ /dev/null @@ -1,323 +0,0 @@ -# Resilience Patterns - -Gopher Orch provides four production-grade resilience patterns: **Retry**, **Timeout**, **Fallback**, and **Circuit Breaker**. These patterns wrap any Runnable to add reliability. - -## Overview - -| Pattern | Purpose | Use Case | -|---------|---------|----------| -| Retry | Repeat on failure | Transient errors, network issues | -| Timeout | Limit execution time | Prevent hanging operations | -| Fallback | Try alternatives | Graceful degradation | -| Circuit Breaker | Prevent cascade failures | Failing external services | - -## Retry - -Automatically retry failed operations with exponential backoff. - -### Basic Usage - -```cpp -#include "gopher/orch/resilience/retry.h" - -using namespace gopher::orch::resilience; - -// Default: 3 attempts, exponential backoff -auto reliable = withRetry(unreliableOperation); - -// Custom policy -auto custom = withRetry(operation, RetryPolicy() - .max_attempts(5) - .initial_delay_ms(100) - .backoff_multiplier(2.0) - .max_delay_ms(10000) - .jitter(true)); -``` - -### RetryPolicy Options - -```cpp -struct RetryPolicy { - uint32_t max_attempts = 3; // Total attempts (including first) - uint64_t initial_delay_ms = 500; // Delay before first retry - double backoff_multiplier = 2.0; // Multiply delay each retry - uint64_t max_delay_ms = 30000; // Cap on delay - bool jitter = true; // Add random jitter (±50%) - - // Optional: only retry specific errors - std::function retry_on; - - // Optional: callback on each retry (for logging) - std::function on_retry; -}; -``` - -### Factory Methods - -```cpp -// Exponential backoff (default) -auto policy = RetryPolicy::exponential(3, 500); - -// Fixed delay (no backoff) -auto policy = RetryPolicy::fixed(5, 1000); -``` - -### Selective Retry - -Only retry specific errors: - -```cpp -auto policy = RetryPolicy(); -policy.retry_on = [](const Error& e) { - // Only retry network errors - return e.code == NetworkError::TIMEOUT || - e.code == NetworkError::CONNECTION_RESET; -}; - -auto reliable = withRetry(operation, policy); -``` - -## Timeout - -Limit execution time for any operation. - -### Basic Usage - -```cpp -#include "gopher/orch/resilience/timeout.h" - -using namespace gopher::orch::resilience; - -// 30 second timeout -auto bounded = withTimeout(slowOperation, 30000); - -// Invoke - returns TIMEOUT error if exceeded -bounded->invoke(input, config, dispatcher, [](Result result) { - if (mcp::holds_alternative(result)) { - auto& error = mcp::get(result); - if (error.code == OrchError::TIMEOUT) { - std::cout << "Operation timed out!" << std::endl; - } - } -}); -``` - -### Nested Timeouts - -Inner timeouts take precedence: - -```cpp -// Outer: 60 seconds -auto outer = withTimeout( - // Inner: 10 seconds (triggers first) - withTimeout(slowOp, 10000), - 60000 -); -``` - -## Fallback - -Try alternative operations on failure. - -### Basic Usage - -```cpp -#include "gopher/orch/resilience/fallback.h" - -using namespace gopher::orch::resilience; - -// Try primary, then fallback -auto safe = withFallback(primaryApi) - .orElse(backupApi) - .orElse(cachedResponse) - .build(); -``` - -### Multiple Fallbacks - -```cpp -auto robust = withFallback(premiumService) - .orElse(standardService) - .orElse(freeService) - .orElse(offlineCache) - .build(); - -// Tries each in order until one succeeds -// Returns FALLBACK_EXHAUSTED if all fail -``` - -### With Different Strategies - -```cpp -// Fast path with slow fallback -auto tiered = withFallback( - withTimeout(fastCache, 100)) // 100ms timeout for cache - .orElse(database) // Fall back to DB - .build(); -``` - -## Circuit Breaker - -Prevent cascade failures by stopping calls to failing services. - -### Basic Usage - -```cpp -#include "gopher/orch/resilience/circuit_breaker.h" - -using namespace gopher::orch::resilience; - -// Default: 5 failures, 30s recovery -auto protected = withCircuitBreaker(externalService); - -// Custom policy -auto custom = withCircuitBreaker(service, CircuitBreakerPolicy() - .failure_threshold(3) - .recovery_timeout_ms(10000) - .half_open_max_calls(2)); -``` - -### Circuit States - -``` - ┌─────────────────────────────────────────┐ - │ │ - │ CLOSED ──(failures >= threshold)──> OPEN - │ │ │ - │ │ │ - │ (success) (recovery timeout) - │ │ │ - │ │ ▼ - │ └─────────── HALF_OPEN <────────────┘ - │ │ - │ (success/failure) - │ │ - └─────────────────────┘ -``` - -- **CLOSED**: Normal operation, requests pass through -- **OPEN**: Failures exceeded threshold, requests immediately rejected -- **HALF_OPEN**: Testing recovery, limited requests allowed - -### CircuitBreakerPolicy Options - -```cpp -struct CircuitBreakerPolicy { - uint32_t failure_threshold = 5; // Failures to open circuit - uint64_t recovery_timeout_ms = 30000; // Time before half-open - uint32_t half_open_max_calls = 3; // Successes to close circuit - - // Optional: callback on state changes - std::function on_state_change; -}; -``` - -### Monitoring State - -```cpp -auto cb = withCircuitBreaker(service, policy); - -// Check state -CircuitState state = cb->state(); -uint32_t failures = cb->failureCount(); - -// Manual reset (for testing/admin) -cb->reset(); -``` - -### Factory Methods - -```cpp -// Standard policy -auto policy = CircuitBreakerPolicy::standard(); - -// Aggressive (quick to open) -auto policy = CircuitBreakerPolicy::aggressive(3, 10000); - -// Lenient (slow to open) -auto policy = CircuitBreakerPolicy::lenient(10, 60000); -``` - -## Combining Patterns - -Patterns can be stacked for comprehensive reliability: - -```cpp -// Full resilience stack -auto robust = withCircuitBreaker( - withFallback( - withRetry( - withTimeout(externalApi, 5000), // 5s timeout - RetryPolicy::exponential(3) // 3 retries - ) - ) - .orElse(cachedResponse) // Fallback to cache - .build(), - CircuitBreakerPolicy::aggressive() // Fast circuit breaker -); -``` - -### Recommended Order - -From inner to outer: -1. **Timeout** - Limit individual attempt time -2. **Retry** - Retry failed attempts -3. **Fallback** - Try alternatives if all retries fail -4. **Circuit Breaker** - Prevent calling failing services - -```cpp -auto stack = - withCircuitBreaker( // 4. Outer: circuit breaker - withFallback( // 3. Try alternatives - withRetry( // 2. Retry on failure - withTimeout( // 1. Inner: timeout each attempt - operation, - 1000), - RetryPolicy::exponential(3))) - .orElse(fallback) - .build()); -``` - -## Observability - -All patterns support callbacks for monitoring: - -```cpp -// Retry logging -RetryPolicy policy; -policy.on_retry = [](const Error& e, uint32_t attempt) { - LOG(INFO) << "Retry attempt " << attempt << ": " << e.message; -}; - -// Circuit breaker state changes -CircuitBreakerPolicy cbPolicy; -cbPolicy.on_state_change = [](CircuitState from, CircuitState to) { - LOG(WARNING) << "Circuit breaker: " << toString(from) - << " -> " << toString(to); -}; -``` - -## Best Practices - -1. **Set appropriate timeouts** - Don't let operations hang indefinitely -2. **Use jitter in retries** - Prevent thundering herd -3. **Configure circuit breakers per service** - Different services need different thresholds -4. **Monitor circuit state** - Alert when circuits open -5. **Test failure scenarios** - Verify resilience works as expected -6. **Have meaningful fallbacks** - Cached data is better than errors - -## Error Codes - -```cpp -namespace OrchError { - TIMEOUT = -100, // Operation timed out - CIRCUIT_OPEN = -101, // Circuit breaker is open - FALLBACK_EXHAUSTED = -102 // All fallback options failed -} -``` - -## See Also - -- [Runnable Interface](Runnable.md) - Core interface -- [Composition Patterns](Composition.md) - Sequence, Parallel, Router -- [Server Abstraction](Server.md) - Building reliable services diff --git a/third_party/gopher-orch/docs/Runnable.md b/third_party/gopher-orch/docs/Runnable.md deleted file mode 100644 index f5e12f4a..00000000 --- a/third_party/gopher-orch/docs/Runnable.md +++ /dev/null @@ -1,222 +0,0 @@ -# Runnable Interface - -The `Runnable` interface is the universal building block for all composable operations in Gopher Orch. Every operation - from simple lambdas to complex AI agents - implements this interface. - -## Overview - -```cpp -template -class Runnable { -public: - virtual std::string name() const = 0; - virtual void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) = 0; -}; -``` - -## Design Principles - -### 1. Async-First - -All operations use callbacks - there are no blocking calls. This enables: -- Non-blocking I/O for network operations -- Efficient use of event loops -- Natural integration with the dispatcher model - -### 2. Dispatcher-Native - -Callbacks are always invoked in dispatcher thread context: -- Thread-safe by design -- No need for locks in most code -- Predictable execution order - -### 3. Type-Safe - -Strong typing with explicit Input/Output types: -- Compile-time type checking -- Clear interfaces between components -- No runtime type errors - -### 4. Composable - -Runnables can be combined using composition patterns: -- `Sequence`: Chain operations (A | B | C) -- `Parallel`: Execute concurrently -- `Router`: Conditional branching -- Resilience wrappers: Retry, Timeout, Fallback, CircuitBreaker - -## Quick Start - -### Creating a Lambda Runnable - -```cpp -#include "gopher/orch/core/lambda.h" - -using namespace gopher::orch::core; - -// Synchronous lambda (simplest form) -auto greet = makeSyncLambda( - [](const std::string& name) -> Result { - return makeSuccess("Hello, " + name + "!"); - }); - -// Async lambda with dispatcher -auto fetch = makeLambda( - [](const std::string& url, Dispatcher& d, ResultCallback cb) { - // Perform async HTTP request... - d.post([cb = std::move(cb)]() { - cb(makeSuccess(JsonValue::object())); - }); - }); -``` - -### Invoking a Runnable - -```cpp -// Get dispatcher (from event loop) -Dispatcher& dispatcher = getDispatcher(); - -// Invoke with callback -greet->invoke("World", RunnableConfig(), dispatcher, - [](Result result) { - if (mcp::holds_alternative(result)) { - std::cout << mcp::get(result) << std::endl; - } else { - std::cerr << mcp::get(result).message << std::endl; - } - }); - -// Run event loop -dispatcher.run(); -``` - -## JsonRunnable - -For dynamic, type-erased operations, use `JsonRunnable`: - -```cpp -using JsonRunnable = Runnable; -using JsonRunnablePtr = std::shared_ptr; -``` - -This is used by: -- Composition patterns (Sequence, Parallel, Router) -- StateGraph nodes -- FFI bindings - -## RunnableConfig - -Configuration passed to every invocation: - -```cpp -struct RunnableConfig { - std::map tags; // Tracing tags - std::map metadata; // Custom metadata - optional timeout; // Operation timeout - - // Create child config for nested operations - RunnableConfig child() const; -}; -``` - -## Implementing Custom Runnables - -### Basic Implementation - -```cpp -class MyRunnable : public Runnable { -public: - std::string name() const override { - return "MyRunnable"; - } - - void invoke(const std::string& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Perform operation... - int result = input.length(); - - // Always post callback to dispatcher - dispatcher.post([callback = std::move(callback), result]() { - callback(makeSuccess(result)); - }); - } -}; -``` - -### Rules for Implementations - -1. **Call callback exactly once** - Either success or error, never both, never zero times -2. **Post to dispatcher** - If not already in dispatcher context, use `dispatcher.post()` -3. **Handle errors gracefully** - Catch exceptions and convert to Error results -4. **Use shared_from_this()** - For capturing `this` in async callbacks - -## Helper Methods - -The base class provides helper methods: - -```cpp -// Post result to dispatcher -template -static void postResult(Dispatcher& dispatcher, - ResultCallback callback, - Result result); - -// Post error to dispatcher -template -static void postError(Dispatcher& dispatcher, - ResultCallback callback, - int code, - const std::string& message); -``` - -## Composition - -Runnables are designed to be composed: - -```cpp -// Chain with pipe operator -auto pipeline = step1 | step2 | step3; - -// Or use builders -auto seq = sequence() - .add(step1) - .add(step2) - .add(step3) - .build(); - -// Add resilience -auto reliable = withRetry(pipeline, RetryPolicy::exponential(3)); -auto bounded = withTimeout(reliable, 30000); // 30 seconds -``` - -## Type Aliases - -Common type aliases for convenience: - -```cpp -// JSON-based runnables -using JsonRunnable = Runnable; -using JsonRunnablePtr = std::shared_ptr; - -// Result callbacks -template -using ResultCallback = std::function)>; -``` - -## Best Practices - -1. **Prefer composition over inheritance** - Use lambdas and composition patterns -2. **Keep runnables focused** - Single responsibility principle -3. **Use descriptive names** - The `name()` method helps debugging -4. **Handle all errors** - Never let exceptions escape -5. **Test with MockServer** - Use mocks for unit testing - -## See Also - -- [Composition Patterns](Composition.md) - Sequence, Parallel, Router -- [Resilience Patterns](Resilience.md) - Retry, Timeout, Fallback, CircuitBreaker -- [Agent Framework](Agent.md) - Building AI agents with tools diff --git a/third_party/gopher-orch/docs/Server.md b/third_party/gopher-orch/docs/Server.md deleted file mode 100644 index 84693e24..00000000 --- a/third_party/gopher-orch/docs/Server.md +++ /dev/null @@ -1,301 +0,0 @@ -# Server Abstraction - -Gopher Orch provides a protocol-agnostic server abstraction. Register tools once, expose via MCP, REST, or Mock protocols interchangeably. - -## Overview - -``` -┌─────────────────────────────────────────┐ -│ Tool Registry │ -│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ -│ │Tool1│ │Tool2│ │Tool3│ │Tool4│ │ -│ └─────┘ └─────┘ └─────┘ └─────┘ │ -└───────────────────┬─────────────────────┘ - │ - ┌─────────┴─────────┐ - │ Server Interface │ - └─────────┬─────────┘ - │ - ┌──────────────┼──────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────┐ ┌───────────┐ ┌───────────┐ -│ MCP │ │ REST │ │ Mock │ -│ Server │ │ Server │ │ Server │ -└─────────┘ └───────────┘ └───────────┘ -``` - -## Tool Registry - -Register tools that can be exposed via any protocol: - -```cpp -#include "gopher/orch/agent/tool_registry.h" - -using namespace gopher::orch::agent; - -auto registry = makeToolRegistry(); - -// Synchronous tool -registry->addSyncTool( - "calculator", - "Perform mathematical calculations", - JsonValue::object({{"expression", "string"}}), - [](const JsonValue& args) -> Result { - auto expr = args["expression"].getString(); - double result = evaluate(expr); - return makeSuccess(JsonValue(result)); - }); - -// Async tool -registry->addTool( - "search", - "Search the web", - JsonValue::object({{"query", "string"}}), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - auto query = args["query"].getString(); - searchWeb(query, d, [cb = std::move(cb)](Result result) { - cb(std::move(result)); - }); - }); -``` - -## MCP Server - -Expose tools via Model Context Protocol: - -```cpp -#include "gopher/orch/server/mcp_server.h" - -using namespace gopher::orch::server; - -// Create MCP server with registry -MCPServerConfig config; -config.name = "my-agent-server"; -config.version = "1.0.0"; - -auto mcpServer = makeMCPServer(registry, config); - -// Listen on TCP -mcpServer->listen("tcp://0.0.0.0:8080"); - -// Or stdio for CLI tools -mcpServer->listen("stdio://"); - -// Run event loop -mcpServer->run(); -``` - -### MCP Server Configuration - -```cpp -struct MCPServerConfig { - std::string name; // Server name - std::string version; // Server version - std::string description; // Human-readable description - - // Capabilities - bool supports_sampling = false; - bool supports_resources = true; - bool supports_prompts = true; - - // Timeouts - uint64_t request_timeout_ms = 30000; - uint64_t session_timeout_ms = 300000; - - // Worker threads - int worker_threads = 4; -}; -``` - -## REST Server - -Expose tools via REST API: - -```cpp -#include "gopher/orch/server/rest_server.h" - -using namespace gopher::orch::server; - -RESTServerConfig config; -config.port = 3000; -config.host = "0.0.0.0"; - -auto restServer = makeRESTServer(registry, config); - -// Tools are exposed as POST endpoints: -// POST /tools/calculator -// POST /tools/search - -restServer->listen(); -restServer->run(); -``` - -### REST API Format - -**Request:** -```http -POST /tools/calculator -Content-Type: application/json - -{ - "expression": "2 + 2" -} -``` - -**Response:** -```json -{ - "success": true, - "result": 4 -} -``` - -**Error Response:** -```json -{ - "success": false, - "error": { - "code": -1, - "message": "Invalid expression" - } -} -``` - -## Mock Server - -For unit testing without network: - -```cpp -#include "gopher/orch/server/mock_server.h" - -using namespace gopher::orch::server; - -auto mockServer = makeMockServer(registry); - -// Set mock responses -mockServer->setToolResponse("search", JsonValue::object({ - {"results", JsonValue::array({...})} -})); - -// Or set errors -mockServer->setToolError("calculator", -1, "Mock error"); - -// Use in tests -auto agent = makeAgent(mockServer); -``` - -### Testing with MockServer - -```cpp -TEST(AgentTest, UsesSearchTool) { - auto registry = makeToolRegistry(); - // ... register tools ... - - auto mockServer = makeMockServer(registry); - mockServer->setToolResponse("search", mockResults); - - auto agent = makeAgent(mockServer); - - auto result = runToCompletion([&](Dispatcher& d, Callback cb) { - agent->invoke("Search for weather", config, d, std::move(cb)); - }); - - EXPECT_TRUE(result["success"].getBool()); - EXPECT_EQ(mockServer->callCount("search"), 1); -} -``` - -## Server Interface - -All servers implement a common interface: - -```cpp -class Server { -public: - virtual ~Server() = default; - - // Get tool specifications - virtual std::vector getTools() const = 0; - - // Execute a tool - virtual void callTool(const std::string& name, - const JsonValue& args, - Dispatcher& dispatcher, - JsonCallback callback) = 0; - - // List available tools - virtual JsonValue listTools() const = 0; -}; -``` - -## Composite Server - -Combine multiple tool sources: - -```cpp -#include "gopher/orch/server/composite_server.h" - -auto composite = makeCompositeServer(); - -// Add local tools -composite->addRegistry(localRegistry); - -// Add remote MCP servers -composite->addMCPClient("tcp://tools-server:8080"); -composite->addMCPClient("tcp://ai-server:8080"); - -// All tools are unified -auto tools = composite->listTools(); -// Returns tools from all sources -``` - -## Tool Approval - -Add human-in-the-loop for sensitive tools: - -```cpp -#include "gopher/orch/human/human_approval.h" - -auto approver = makeHumanApproval(); - -// Require approval for specific tools -approver->requireApproval("delete_file"); -approver->requireApproval("send_email"); - -// Set approval handler -approver->setHandler([](const ToolCall& call) -> bool { - std::cout << "Approve " << call.name << "? (y/n): "; - char response; - std::cin >> response; - return response == 'y'; -}); - -// Wrap server with approval -auto protected = withApproval(server, approver); -``` - -## Best Practices - -1. **Use MockServer for tests** - No network dependencies in unit tests -2. **Define schemas** - Validate tool arguments -3. **Handle errors gracefully** - Return meaningful error messages -4. **Set timeouts** - Prevent hanging tool calls -5. **Log tool usage** - For debugging and auditing -6. **Version your API** - Include version in server config - -## Protocol Comparison - -| Feature | MCP | REST | Mock | -|---------|-----|------|------| -| Streaming | Yes (SSE) | No | N/A | -| Bi-directional | Yes | No | N/A | -| Discovery | Built-in | Custom | N/A | -| Authentication | Protocol-level | HTTP-based | N/A | -| Best for | AI agents | Web services | Testing | - -## See Also - -- [Tool Registry](ToolRegistry.md) - Detailed tool registration guide -- [Agent Framework](Agent.md) - Using servers with agents -- [FFI Guide](FFI.md) - Cross-language server integration diff --git a/third_party/gopher-orch/docs/StateGraph.md b/third_party/gopher-orch/docs/StateGraph.md deleted file mode 100644 index f4c1d14e..00000000 --- a/third_party/gopher-orch/docs/StateGraph.md +++ /dev/null @@ -1,305 +0,0 @@ -# StateGraph Guide - -StateGraph provides LangGraph-style stateful workflows with conditional edges. It implements the Pregel model (Bulk Synchronous Parallel) for deterministic, reproducible execution. - -## Overview - -StateGraph enables: -- **Stateful execution** - Maintain state across nodes -- **Conditional transitions** - Branch based on state -- **Cyclic workflows** - Loops and iterations -- **Composable nodes** - Any Runnable can be a node - -## Quick Start - -```cpp -#include "gopher/orch/graph/state_graph.h" - -using namespace gopher::orch::graph; - -// Define graph -StateGraph graph; -graph - .addNode("agent", agentNode) - .addNode("tools", toolsNode) - .addEdge(StateGraph::START(), "agent") - .addConditionalEdge("agent", [](const GraphState& state) { - if (state.get("should_continue").getBool()) { - return "tools"; - } - return StateGraph::END(); - }) - .addEdge("tools", "agent"); - -// Compile and execute -auto compiled = graph.compile(); -compiled->invoke(initialState, config, dispatcher, callback); -``` - -## GraphState - -State is stored as a JSON-like key-value structure: - -```cpp -GraphState state; - -// Set values -state.set("messages", JsonValue::array()); -state.set("step_count", 0); -state.set("status", "running"); - -// Get values -auto messages = state.get("messages"); -auto count = state.get("step_count").getInt(); - -// Convert to/from JSON -JsonValue json = state.toJson(); -GraphState restored = GraphState::fromJson(json); -``` - -## Adding Nodes - -### Synchronous Lambda - -```cpp -graph.addNode("increment", [](const GraphState& state) { - GraphState result = state; - int count = state.get("count").getInt(); - result.set("count", count + 1); - return result; -}); -``` - -### Async Lambda - -```cpp -graph.addNodeAsync("fetch", [](const GraphState& state, - const RunnableConfig& config, - Dispatcher& dispatcher, - GraphStateCallback callback) { - // Perform async operation - fetchData(state.get("url").getString(), dispatcher, - [state, callback = std::move(callback)](Result result) { - if (mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - return; - } - GraphState newState = state; - newState.set("data", mcp::get(result)); - callback(makeSuccess(std::move(newState))); - }); -}); -``` - -### JsonRunnable Node - -```cpp -// Any JsonRunnable can be a node -auto llmRunnable = makeLLMRunnable(provider, config); -graph.addNode("llm", llmRunnable); - -// The runnable receives state as JSON, returns updates -// Output keys are merged into state -``` - -## Adding Edges - -### Direct Edges - -Always transition from one node to another: - -```cpp -graph.addEdge("start", "process"); // start -> process -graph.addEdge("process", "end"); // process -> end -``` - -### Conditional Edges - -Transition based on state evaluation: - -```cpp -graph.addConditionalEdge("agent", [](const GraphState& state) -> std::string { - auto action = state.get("action").getString(); - - if (action == "search") return "search_node"; - if (action == "calculate") return "calc_node"; - if (action == "done") return StateGraph::END(); - - return "error_node"; // Default -}); -``` - -### Special Nodes - -```cpp -// START - entry point (implicit) -graph.addEdge(StateGraph::START(), "first_node"); - -// END - terminates execution -graph.addEdge("last_node", StateGraph::END()); -``` - -## Execution Model - -StateGraph uses the **Pregel model**: - -1. **PLAN** - Determine which nodes can execute -2. **EXECUTE** - Run scheduled nodes in parallel -3. **UPDATE** - Apply state changes atomically -4. **REPEAT** - Continue until END is reached - -``` -┌─────────────────────────────────────────┐ -│ Execution Loop │ -├─────────────────────────────────────────┤ -│ 1. PLAN: Find ready nodes │ -│ - Check edges from current position │ -│ - Evaluate conditional edges │ -│ │ -│ 2. EXECUTE: Run nodes │ -│ - Execute node functions │ -│ - Collect state updates │ -│ │ -│ 3. UPDATE: Merge state │ -│ - Apply updates atomically │ -│ - Determine next nodes │ -│ │ -│ 4. Check: END reached? │ -│ - Yes: Return final state │ -│ - No: Loop to step 1 │ -└─────────────────────────────────────────┘ -``` - -## ReAct Agent Example - -Build a reasoning agent with tool usage: - -```cpp -StateGraph graph; - -// Agent node - decides what to do -graph.addNode("agent", [&llm](const GraphState& state) { - // Call LLM with messages - auto response = llm->chat(state.get("messages")); - - GraphState result = state; - auto messages = state.get("messages"); - messages.push_back(response.message.toJson()); - result.set("messages", messages); - - // Check if agent wants to use tools - if (response.hasToolCalls()) { - result.set("tool_calls", response.toolCallsJson()); - result.set("should_continue", true); - } else { - result.set("should_continue", false); - } - - return result; -}); - -// Tools node - executes tool calls -graph.addNode("tools", [&executor](const GraphState& state) { - auto calls = state.get("tool_calls"); - auto results = executor->execute(calls); - - GraphState result = state; - auto messages = state.get("messages"); - for (auto& r : results) { - messages.push_back(r.toJson()); - } - result.set("messages", messages); - result.set("tool_calls", JsonValue::null()); - - return result; -}); - -// Wire up the graph -graph.addEdge(StateGraph::START(), "agent") - .addConditionalEdge("agent", [](const GraphState& s) { - return s.get("should_continue").getBool() ? "tools" : StateGraph::END(); - }) - .addEdge("tools", "agent"); - -// Compile and run -auto agent = graph.compile(); -``` - -## Compiled Graph - -The compiled graph is a `Runnable`: - -```cpp -auto compiled = graph.compile(); - -// It's just a Runnable - compose it! -auto withTimeout = withTimeout(compiled, 60000); -auto withRetry = withRetry(compiled, RetryPolicy::exponential(3)); - -// Or put it in a sequence -auto pipeline = sequence() - .add(prepareInput) - .add(compiled) - .add(formatOutput) - .build(); -``` - -## State Reducers - -For custom state merging logic (like LangGraph's `add_messages`): - -```cpp -// Define custom state with reducer -struct AgentState { - std::vector messages; // APPEND semantics - int step_count; // LAST_WRITE_WINS - Usage total_usage; // ACCUMULATE - - // Reducer merges updates into current state - static AgentState reduce(const AgentState& current, - const AgentState& update) { - AgentState result; - - // APPEND: messages - result.messages = current.messages; - for (const auto& msg : update.messages) { - result.messages.push_back(msg); - } - - // LAST_WRITE_WINS: step_count - result.step_count = update.step_count; - - // ACCUMULATE: usage - result.total_usage.prompt_tokens = - current.total_usage.prompt_tokens + update.total_usage.prompt_tokens; - - return result; - } -}; -``` - -## Best Practices - -1. **Keep nodes focused** - Each node should do one thing -2. **Use meaningful node names** - Helps with debugging and tracing -3. **Handle errors in nodes** - Return errors via callback -4. **Avoid shared mutable state** - Let the graph manage state -5. **Test nodes independently** - Unit test before composing -6. **Set max iterations** - Prevent infinite loops - -## Debugging - -```cpp -// Enable step callbacks -auto compiled = graph.compile(); -compiled->setStepCallback([](const std::string& node, const GraphState& state) { - std::cout << "Executed node: " << node << std::endl; - std::cout << "State: " << state.toJson().toString() << std::endl; -}); -``` - -## See Also - -- [Runnable Interface](Runnable.md) - Core interface -- [Agent Framework](Agent.md) - ReAct agents with tools -- [Composition Patterns](Composition.md) - Sequence, Parallel, Router diff --git a/third_party/gopher-orch/docs/ToolRegistry.md b/third_party/gopher-orch/docs/ToolRegistry.md deleted file mode 100644 index 459a7543..00000000 --- a/third_party/gopher-orch/docs/ToolRegistry.md +++ /dev/null @@ -1,485 +0,0 @@ -# ToolRegistry & ToolExecutor Design Document - -## Overview - -The tool management system is split into two components following the Single Responsibility Principle: - -- **ToolRegistry** - A pure repository that stores and retrieves tool definitions -- **ToolExecutor** - Executes tools by looking them up in a registry - -This separation ensures clean architecture where storage concerns are decoupled from execution logic. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Application / Agent │ -└─────────────────────────────────────────────────────────────────────┘ - │ │ - │ getToolSpecs() │ executeToolCalls() - ▼ ▼ -┌───────────────────────────────┐ ┌───────────────────────────────┐ -│ ToolRegistry │◀──│ ToolExecutor │ -│ (Repository / Storage) │ │ (Execution Logic) │ -├───────────────────────────────┤ ├───────────────────────────────┤ -│ • addTool() │ │ • executeTool() │ -│ • addServer() │ │ • executeToolCall() │ -│ • addSyncTool() │ │ • executeToolCalls() │ -│ • getToolSpecs() │ │ │ -│ • getToolEntry() │ │ Uses registry->getToolEntry() │ -│ • hasTool() │ │ to lookup before execution │ -│ • loadFromFile() │ │ │ -└───────────────────────────────┘ └───────────────────────────────┘ - │ │ │ │ - ▼ ▼ ▼ ▼ -┌──────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ -│ Local Tools │ │ MCP Server │ │ MCP Server │ │ REST Tools │ -│ (Lambda) │ │ (STDIO) │ │ (HTTP) │ │ (Adapter) │ -└──────────────┘ └────────────┘ └────────────┘ └────────────┘ -``` - -## Core Components - -### 1. ToolRegistry - Repository - -```cpp -class ToolRegistry { - public: - // Registration - void addTool(name, description, parameters, function); - void addSyncTool(name, description, parameters, sync_function); - void addServer(server, dispatcher); - void addServerTool(server, tool_info, alias); - - // Retrieval - std::vector getToolSpecs() const; - optional getToolSpec(name) const; - optional getToolEntry(name) const; - bool hasTool(name) const; - std::vector getToolNames() const; - size_t toolCount() const; - - // Management - void removeTool(name); - void clear(); - - // Configuration - void loadFromFile(path, dispatcher, callback); - void loadFromString(json_string, dispatcher, callback); - void setEnv(name, value); -}; -``` - -### 2. ToolExecutor - Execution - -```cpp -class ToolExecutor { - public: - explicit ToolExecutor(ToolRegistryPtr registry); - - // Get underlying registry - ToolRegistryPtr registry() const; - - // Execute single tool - void executeTool(name, arguments, dispatcher, callback); - - // Execute ToolCall from LLM - void executeToolCall(call, dispatcher, callback); - - // Execute multiple tool calls (parallel) - void executeToolCalls(calls, parallel, dispatcher, callback); -}; -``` - -### 3. ToolEntry - Internal Representation - -```cpp -struct ToolEntry { - ToolSpec spec; // Name, description, parameters - ToolFunction function; // Lambda for local tools - ServerPtr server; // MCP server for remote tools - std::string original_name; // Original name on server - - bool isLocal() const { return server == nullptr; } - bool isRemote() const { return server != nullptr; } -}; -``` - -## Tool Registration Flow - -``` -┌────────────┐ ┌──────────────┐ ┌─────────────┐ -│ Source │────▶│ ToolRegistry │────▶│ ToolEntry │ -└────────────┘ └──────────────┘ └─────────────┘ - │ │ │ - │ │ │ - ▼ ▼ ▼ - -╔═══════════════════════════════════════════════════════════════════╗ -║ LOCAL TOOL REGISTRATION ║ -╠═══════════════════════════════════════════════════════════════════╣ -║ ║ -║ registry->addTool("name", "desc", schema, lambda) ║ -║ │ ║ -║ ▼ ║ -║ ┌─────────────────────┐ ║ -║ │ Create ToolEntry │ ║ -║ │ • spec.name = name │ ║ -║ │ • spec.desc = desc │ ║ -║ │ • function = lambda │ ║ -║ │ • server = nullptr │ ║ -║ └─────────────────────┘ ║ -║ │ ║ -║ ▼ ║ -║ tools_[name] = entry ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════╝ - -╔═══════════════════════════════════════════════════════════════════╗ -║ MCP SERVER REGISTRATION ║ -╠═══════════════════════════════════════════════════════════════════╣ -║ ║ -║ registry->addServer(server, dispatcher) ║ -║ │ ║ -║ ▼ ║ -║ ┌────────────────────────┐ ║ -║ │ server->listTools() │──────▶ Async tool discovery ║ -║ └────────────────────────┘ ║ -║ │ ║ -║ ▼ ║ -║ For each ServerToolInfo: ║ -║ ┌─────────────────────────────┐ ║ -║ │ Create ToolEntry │ ║ -║ │ • spec = toToolSpec(info) │ ║ -║ │ • server = server │ ║ -║ │ • original_name = info.name │ ║ -║ └─────────────────────────────┘ ║ -║ │ ║ -║ ▼ ║ -║ tools_["server:name"] = entry (prefixed) ║ -║ tools_["name"] = entry (if no conflict) ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════╝ -``` - -## Tool Execution Flow - -``` -┌─────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ -│ Agent │────▶│ ToolExecutor │────▶│ ToolRegistry │────▶│ Result │ -└─────────┘ └──────────────┘ └──────────────┘ └──────────┘ - │ │ │ │ - │ executeTool() │ │ │ - │ ───────────────▶│ │ │ - │ │ getToolEntry() │ │ - │ │ ──────────────────▶│ │ - │ │ │ │ - │ │◀──────────────────── │ │ - │ │ ToolEntry │ │ - │ │ │ │ - │ │ if entry.isLocal() │ │ - │ │ ┌─────────────────────────────────┐ │ - │ │ │ entry.function(args, dispatcher,│ │ - │ │ │ callback) │ │ - │ │ └─────────────────────────────────┘ │ - │ │ │ │ - │ │ if entry.isRemote()│ │ - │ │ ┌─────────────────────────────────┐ │ - │ │ │ entry.server->callTool( │ │ - │ │ │ original_name, args, │ │ - │ │ │ config, dispatcher, callback) │ │ - │ │ └─────────────────────────────────┘ │ - │ │ │ │ - │ ◀────────────────────────────────────────────────────── │ - │ callback(Result) │ -``` - -## Parallel Tool Execution - -``` -┌─────────┐ ┌──────────────┐ -│ Agent │────▶│ ToolExecutor │ -└─────────┘ └──────────────┘ - │ │ - │ executeToolCalls(calls, parallel=true) - │ ─────────────────────────────────────▶ - │ │ - │ │ ┌─────────────────────────────────────────┐ - │ │ │ Create shared state: │ - │ │ │ • results = vector(calls.size())│ - │ │ │ • pending = atomic(calls.size()) │ - │ │ └─────────────────────────────────────────┘ - │ │ - │ │ For each call (parallel): - │ │ ┌────────────────────────────────────────┐ - │ │ │ registry->getToolEntry(call.name) │ - │ │ │ execute entry.function or server call │ - │ │ │ on completion: results[i] = result │ - │ │ │ if (--pending == 0) │ - │ │ │ callback(results) │ - │ │ └────────────────────────────────────────┘ - │ │ - │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ - │ │ │ Tool 1 │ │ Tool 2 │ │ Tool 3 │ - │ │ │ ───────▶│ │ ───────▶│ │ ───────▶│ - │ │ └─────────┘ └─────────┘ └─────────┘ - │ │ │ │ │ - │ │ └────────────┴────────────┘ - │ │ │ - │ │ All complete: pending == 0 - │ │ │ - │ ◀────────────────────────────────┘ - │ callback(vector>) -``` - -## Example Usage - -### Basic Setup - -```cpp -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/agent/tool_executor.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::core; - -// Create registry and executor -auto registry = makeToolRegistry(); -auto executor = makeToolExecutor(registry); -``` - -### Adding Local Tools - -```cpp -// Async tool with lambda -JsonValue calcSchema = JsonValue::object(); -calcSchema["type"] = "object"; -// ... schema definition ... - -registry->addTool("add", "Add two numbers", calcSchema, - [](const JsonValue& args, Dispatcher& dispatcher, JsonCallback callback) { - double a = args["a"].getDouble(); - double b = args["b"].getDouble(); - - JsonValue result = JsonValue::object(); - result["sum"] = a + b; - - dispatcher.post([callback = std::move(callback), result]() { - callback(Result(result)); - }); - }); - -// Sync tool (wrapper created automatically) -registry->addSyncTool("multiply", "Multiply two numbers", calcSchema, - [](const JsonValue& args) -> Result { - double a = args["a"].getDouble(); - double b = args["b"].getDouble(); - - JsonValue result = JsonValue::object(); - result["product"] = a * b; - return Result(result); - }); -``` - -### Adding MCP Server Tools - -```cpp -#include "gopher/orch/server/mcp_server.h" - -// Create MCP server -auto weatherServer = createMCPServer("weather", "weather-service", {"--port", "8080"}); - -// Connect and add all tools (async discovery) -registry->addServer(weatherServer, dispatcher); - -// Or add specific tools by name -registry->addServerTool(weatherServer, "get_forecast", "forecast"); - -// Or provide tool list directly (sync) -std::vector tools = { - ServerToolInfo{"get_weather", "Get current weather", weatherSchema}, - ServerToolInfo{"get_forecast", "Get weather forecast", forecastSchema} -}; -registry->addServer(weatherServer, tools); -``` - -### Executing Tools - -```cpp -// Execute single tool via executor -JsonValue args = JsonValue::object(); -args["a"] = 10; -args["b"] = 20; - -executor->executeTool("add", args, dispatcher, - [](Result result) { - if (mcp::holds_alternative(result)) { - auto& value = mcp::get(result); - std::cout << "Result: " << value.toString() << std::endl; - } - }); - -// Execute tool call from LLM -ToolCall call("call_123", "search", JsonValue::object()); -call.arguments["query"] = "weather in NYC"; - -executor->executeToolCall(call, dispatcher, - [](Result result) { - // Handle result... - }); - -// Execute multiple tool calls in parallel -std::vector calls = { - ToolCall("call_1", "get_weather", weatherArgs), - ToolCall("call_2", "get_time", timeArgs) -}; - -executor->executeToolCalls(calls, true /* parallel */, dispatcher, - [](std::vector> results) { - for (size_t i = 0; i < results.size(); ++i) { - if (mcp::holds_alternative(results[i])) { - std::cout << "Tool " << i << " result: " - << mcp::get(results[i]).toString() << std::endl; - } - } - }); -``` - -### Using with Agent - -```cpp -#include "gopher/orch/agent/agent.h" -#include "gopher/orch/llm/openai_provider.h" - -// Create components -auto provider = OpenAIProvider::create("sk-..."); -auto registry = makeToolRegistry(); - -// Add tools to registry -registry->addSyncTool("calculator", "Perform math", mathSchema, - [](const JsonValue& args) -> Result { - // Implementation... - }); - -// Create agent with registry -// Agent internally creates its own ToolExecutor -auto agent = ReActAgent::create(provider, registry); - -// Run query - agent will use tools automatically -agent->run("What is 25 * 4?", dispatcher, - [](Result result) { - if (mcp::holds_alternative(result)) { - auto& agentResult = mcp::get(result); - std::cout << "Answer: " << agentResult.response << std::endl; - } - }); -``` - -### Loading from JSON Configuration - -```cpp -// Load from file -registry->loadFromFile("tools.json", dispatcher, - [](VoidResult result) { - if (mcp::holds_alternative(result)) { - std::cout << "Tools loaded successfully!" << std::endl; - } else { - auto& error = mcp::get(result); - std::cerr << "Failed to load: " << error.message << std::endl; - } - }); -``` - -## JSON Configuration Schema - -```json -{ - "name": "registry-name", - "base_url": "https://api.example.com", - "default_headers": { - "User-Agent": "MyApp/1.0" - }, - - "auth_presets": { - "main_api": { - "type": "bearer", - "value": "${API_TOKEN}" - } - }, - - "mcp_servers": [ - { - "name": "weather", - "transport": "stdio", - "command": "/usr/local/bin/weather-server", - "args": ["--format", "json"], - "env": { - "API_KEY": "${WEATHER_API_KEY}" - } - } - ], - - "tools": [ - { - "name": "search_web", - "description": "Search the web for information", - "input_schema": { - "type": "object", - "properties": { - "query": { "type": "string" } - }, - "required": ["query"] - }, - "rest_endpoint": { - "method": "GET", - "url": "${BASE_URL}/search", - "query_params": { "q": "$.query" }, - "response_path": "$.results" - } - }, - { - "name": "get_forecast", - "description": "Get weather forecast from MCP server", - "input_schema": { - "type": "object", - "properties": { - "city": { "type": "string" } - }, - "required": ["city"] - }, - "mcp_reference": { - "server_name": "weather", - "tool_name": "forecast" - } - } - ] -} -``` - -## Thread Safety - -- **ToolRegistry**: Configuration methods (`addTool`, `addServer`) should be called before use. Read methods (`getToolSpecs`, `getToolEntry`) are thread-safe after configuration. -- **ToolExecutor**: All execution methods are thread-safe. -- All callbacks are invoked in the dispatcher thread context. - -## Error Handling - -```cpp -executor->executeTool("nonexistent", args, dispatcher, - [](Result result) { - if (!mcp::holds_alternative(result)) { - auto& error = mcp::get(result); - std::cerr << "Error: " << error.message << std::endl; - } - }); -``` - -## Best Practices - -1. **Separate concerns** - Use ToolRegistry for storage, ToolExecutor for execution -2. **Register tools before starting agent** - Tool discovery is async -3. **Use meaningful tool names** - LLMs use names to decide which tool to call -4. **Provide clear descriptions** - Help LLM understand when to use each tool -5. **Define precise schemas** - Reduce invalid argument errors -6. **Handle errors gracefully** - Tool failures are passed to LLM for recovery -7. **Use prefixed names** for MCP tools to avoid conflicts (`server:tool`) diff --git a/third_party/gopher-orch/examples/CMakeLists.txt b/third_party/gopher-orch/examples/CMakeLists.txt deleted file mode 100644 index da64143f..00000000 --- a/third_party/gopher-orch/examples/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# gopher-orch examples - -# Hello World example (basic orch functionality) -add_subdirectory(hello_world) - -# MCP Client example (demonstrates gopher-mcp integration) -add_subdirectory(mcp_client) - -# SDK examples (ToolsFetcher, agent usage, and gateway server) -add_subdirectory(sdk) diff --git a/third_party/gopher-orch/examples/chatbot/README.md b/third_party/gopher-orch/examples/chatbot/README.md deleted file mode 100644 index 2bb97c05..00000000 --- a/third_party/gopher-orch/examples/chatbot/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# Multi-turn Conversational Agent Example - -A chatbot that maintains conversation history and can use tools across multiple turns. - -## What This Example Shows - -- Maintaining conversation context across turns -- Building input with message history -- Using tools within conversation flow -- Interactive REPL-style interface -- Conversation reset functionality - -## Running - -```bash -# Build -cd build -make chatbot - -# Run (requires OpenAI API key) -OPENAI_API_KEY=sk-... ./bin/chatbot -``` - -## Expected Output - -``` -Chatbot ready! Type 'quit' to exit, 'reset' to clear history. -======================================== - -You: Hello! -Assistant: Hi there! How can I help you today? - -You: What time is it? - -Assistant: Let me check the time for you. - -[Calling tool: get_time] - -The current time is 2:30 PM. Is there anything else you would like to know? - -You: Remember that my favorite color is blue - -Assistant: [Calling tool: remember] - -I have noted that your favorite color is blue. I will remember this for our conversation. - -You: reset -Conversation reset. - -You: quit - -Goodbye! -``` - -## Code Walkthrough - -### 1. Chatbot Class -```cpp -class Chatbot { - public: - Chatbot(LLMProviderPtr provider, ToolRegistryPtr registry); - void chat(const std::string& user_message, - Dispatcher& dispatcher, - std::function on_response); - void reset(); - private: - std::vector conversation_; -}; -``` - -### 2. Conversation Management -```cpp -// Add user message to history -conversation_.push_back(Message::user(user_message)); - -// Build context from history -JsonValue context = JsonValue::array(); -for (const auto& msg : conversation_) { - JsonValue msg_json = JsonValue::object(); - msg_json["role"] = roleToString(msg.role); - msg_json["content"] = msg.content; - context.push_back(msg_json); -} -``` - -### 3. Interactive Loop -```cpp -while (true) { - std::getline(std::cin, line); - if (line == "quit") break; - if (line == "reset") { - chatbot.reset(); - continue; - } - chatbot.chat(line, dispatcher, on_response); -} -``` - -## Key Concepts - -- **Message History**: Stores all messages for context -- **System Message**: Initial prompt defining assistant behavior -- **Tool Integration**: Tools available across conversation turns -- **Reset**: Clears history while keeping system prompt - -## See Also - -- [Agent Framework](../../docs/Agent.md) -- [Simple Agent Example](../simple_agent/) diff --git a/third_party/gopher-orch/examples/chatbot/main.cc b/third_party/gopher-orch/examples/chatbot/main.cc deleted file mode 100644 index 21e32258..00000000 --- a/third_party/gopher-orch/examples/chatbot/main.cc +++ /dev/null @@ -1,154 +0,0 @@ -// Multi-turn Conversational Agent Example -// -// Demonstrates a chatbot that maintains conversation history -// and can use tools across multiple turns. - -#include -#include - -#include "gopher/orch/orch.h" - -using namespace gopher::orch; -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -class Chatbot { - public: - Chatbot(LLMProviderPtr provider, ToolRegistryPtr registry) - : provider_(std::move(provider)), registry_(std::move(registry)) { - // Initialize conversation with system message - conversation_.push_back( - Message::system("You are a helpful conversational assistant. " - "You can use tools when needed. " - "Remember context from previous messages.")); - } - - // Process a user message and return the response - void chat(const std::string& user_message, - Dispatcher& dispatcher, - std::function on_response) { - // Add user message to conversation - conversation_.push_back(Message::user(user_message)); - - // Create agent for this turn - auto executor = makeToolExecutor(registry_); - auto agent = AgentRunnable::create( - provider_, executor, AgentConfig("gpt-4").withMaxIterations(5)); - - // Build input with conversation context - JsonValue input = JsonValue::object(); - JsonValue context = JsonValue::array(); - for (const auto& msg : conversation_) { - JsonValue msg_json = JsonValue::object(); - msg_json["role"] = roleToString(msg.role); - msg_json["content"] = msg.content; - context.push_back(msg_json); - } - input["context"] = context; - input["query"] = ""; // Query is already in context - - agent->invoke( - input, RunnableConfig(), dispatcher, - [this, on_response = std::move(on_response)](Result result) { - if (mcp::holds_alternative(result)) { - on_response("Error: " + mcp::get(result).message); - return; - } - - auto& output = mcp::get(result); - std::string response = output["response"].getString(); - - // Add assistant response to conversation history - conversation_.push_back(Message::assistant(response)); - - on_response(response); - }); - } - - // Get conversation history - const std::vector& history() const { return conversation_; } - - // Clear conversation (start fresh) - void reset() { - conversation_.clear(); - conversation_.push_back( - Message::system("You are a helpful conversational assistant.")); - } - - private: - LLMProviderPtr provider_; - ToolRegistryPtr registry_; - std::vector conversation_; -}; - -int main() { - const char* api_key = std::getenv("OPENAI_API_KEY"); - if (!api_key) { - std::cerr << "Error: OPENAI_API_KEY environment variable not set\n"; - return 1; - } - - auto dispatcher = mcp::event::createLibeventDispatcher(); - - // Create provider and registry - auto provider = makeOpenAIProvider(api_key, "gpt-4"); - auto registry = makeToolRegistry(); - - // Add some tools - registry->addSyncTool( - "remember", "Remember a fact for later. Input: {\"fact\": \"...\"}", - JsonValue::object(), [](const JsonValue& args) -> Result { - // In real app, would store to memory - return makeSuccess( - JsonValue("Remembered: " + args["fact"].getString())); - }); - - registry->addSyncTool( - "get_time", "Get current time", JsonValue::object(), - [](const JsonValue&) -> Result { - return makeSuccess(JsonValue("Current time: 2:30 PM")); - }); - - // Create chatbot - Chatbot chatbot(provider, registry); - - std::cout - << "Chatbot ready! Type 'quit' to exit, 'reset' to clear history.\n"; - std::cout << "========================================\n\n"; - - // Interactive loop - std::string line; - while (true) { - std::cout << "You: "; - std::getline(std::cin, line); - - if (line == "quit" || line == "exit") { - break; - } - - if (line == "reset") { - chatbot.reset(); - std::cout << "Conversation reset.\n\n"; - continue; - } - - if (line.empty()) { - continue; - } - - bool done = false; - chatbot.chat(line, *dispatcher, [&done](std::string response) { - std::cout << "\nAssistant: " << response << "\n\n"; - done = true; - }); - - // Run until response received - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - std::cout << "\nGoodbye!\n"; - return 0; -} diff --git a/third_party/gopher-orch/examples/hello_world/CMakeLists.txt b/third_party/gopher-orch/examples/hello_world/CMakeLists.txt deleted file mode 100644 index 236c04f8..00000000 --- a/third_party/gopher-orch/examples/hello_world/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -add_executable(hello_world_example main.cpp) - -target_link_libraries(hello_world_example - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -target_include_directories(hello_world_example PRIVATE - ${CMAKE_SOURCE_DIR}/include - ${GOPHER_MCP_INCLUDE_DIR} -) - -# Examples are not installed by default -# To install, use: cmake --install . --component examples diff --git a/third_party/gopher-orch/examples/hello_world/main.cpp b/third_party/gopher-orch/examples/hello_world/main.cpp deleted file mode 100644 index 5c79dbd8..00000000 --- a/third_party/gopher-orch/examples/hello_world/main.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include -#include -#include - -#include "orch/core/hello.h" -#include "orch/core/version.h" - -using namespace gopher::orch::core; - -int main(int argc, char* argv[]) { - std::cout << "gopher-orch version: " << Version::string() << std::endl; - std::cout << "----------------------------------------" << std::endl; - - // Basic usage - { - std::cout << "\n1. Basic Hello usage:" << std::endl; - Hello hello; - std::cout << " " << hello.greet() << std::endl; - - hello.set_name("gopher-orch User"); - std::cout << " " << hello.greet() << std::endl; - } - - // Constructor with parameter - { - std::cout << "\n2. Parameterized constructor:" << std::endl; - Hello hello("Alice"); - std::cout << " " << hello.greet() << std::endl; - } - - // Custom prefix - { - std::cout << "\n3. Custom prefix greetings:" << std::endl; - Hello hello("Bob"); - std::cout << " " << hello.greet_with_prefix("Hi") << std::endl; - std::cout << " " << hello.greet_with_prefix("Welcome") << std::endl; - std::cout << " " << hello.greet_with_prefix("Greetings") << std::endl; - } - - // Builder pattern - { - std::cout << "\n4. Using HelloBuilder:" << std::endl; - HelloBuilder builder; - - auto hello1 = builder.with_name("Charlie").build(); - std::cout << " " << hello1->greet() << std::endl; - - auto hello2 = - builder.with_name("Diana").with_greeting_style("formal").build(); - std::cout << " " << hello2->greet() << std::endl; - } - - // Command line argument - if (argc > 1) { - std::cout << "\n5. Using command line argument:" << std::endl; - Hello hello(argv[1]); - std::cout << " " << hello.greet() << std::endl; - } - - // Multiple instances - { - std::cout << "\n6. Multiple instances:" << std::endl; - std::vector> hellos; - - hellos.push_back(std::make_unique("User1")); - hellos.push_back(std::make_unique("User2")); - hellos.push_back(std::make_unique("User3")); - - for (const auto& hello : hellos) { - std::cout << " " << hello->greet() << std::endl; - } - } - - std::cout << "\n----------------------------------------" << std::endl; - std::cout << "Example completed successfully!" << std::endl; - - return 0; -} diff --git a/third_party/gopher-orch/examples/mcp_client/CMakeLists.txt b/third_party/gopher-orch/examples/mcp_client/CMakeLists.txt deleted file mode 100644 index 6437f789..00000000 --- a/third_party/gopher-orch/examples/mcp_client/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -# MCP Client Example -# Demonstrates gopher-mcp integration with gopher-orch -cmake_minimum_required(VERSION 3.10) - -# Define the executable -add_executable(mcp_client_example - mcp_client_example.cc -) - -# Set target properties -set_target_properties(mcp_client_example PROPERTIES - CXX_STANDARD 14 - CXX_STANDARD_REQUIRED ON - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" -) - -# Link against gopher-orch and gopher-mcp libraries -target_link_libraries(mcp_client_example PRIVATE - gopher-orch-static - gopher-mcp - gopher-mcp-event - ${CMAKE_THREAD_LIBS_INIT} -) - -# Include directories -target_include_directories(mcp_client_example PRIVATE - ${CMAKE_SOURCE_DIR}/include - ${GOPHER_MCP_INCLUDE_DIR} -) - -# Examples are not installed by default -# To install, use: cmake --install . --component examples diff --git a/third_party/gopher-orch/examples/mcp_client/mcp_client_example.cc b/third_party/gopher-orch/examples/mcp_client/mcp_client_example.cc deleted file mode 100644 index 5cc1937a..00000000 --- a/third_party/gopher-orch/examples/mcp_client/mcp_client_example.cc +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @file mcp_client_example.cc - * @brief Example demonstrating gopher-orch integration with gopher-mcp - * - * This example shows how gopher-orch can extend and use gopher-mcp - * functionality. It demonstrates: - * 1. Using gopher-orch's Hello class - * 2. Using gopher-mcp types and utilities - * 3. Integration between both libraries - */ - -#include -#include -#include - -// gopher-orch includes -#include "orch/core/hello.h" -#include "orch/core/version.h" - -// gopher-mcp includes -#include "mcp/json/json_bridge.h" -#include "mcp/types.h" - -using namespace gopher::orch::core; -using namespace mcp; - -int main(int argc, char* argv[]) { - std::cout << "=== gopher-orch + gopher-mcp Integration Example ===" - << std::endl; - std::cout << std::endl; - - // Show versions - std::cout << "Versions:" << std::endl; - std::cout << " gopher-orch: " << Version::string() << std::endl; - std::cout << std::endl; - - // Demonstrate gopher-orch Hello class - std::cout << "1. gopher-orch Hello class:" << std::endl; - Hello hello("MCP User"); - std::cout << " " << hello.greet() << std::endl; - std::cout << std::endl; - - // Demonstrate gopher-mcp types - std::cout << "2. gopher-mcp types:" << std::endl; - - // Create a Tool definition - Tool calculator_tool; - calculator_tool.name = "calculator"; - calculator_tool.description = mcp::make_optional( - std::string("A simple calculator tool for basic arithmetic")); - - // Create input schema - json::JsonValue schema; - schema["type"] = "object"; - schema["properties"]["operation"]["type"] = "string"; - schema["properties"]["a"]["type"] = "number"; - schema["properties"]["b"]["type"] = "number"; - - auto required_arr = json::JsonValue::array(); - required_arr.push_back("operation"); - required_arr.push_back("a"); - required_arr.push_back("b"); - schema["required"] = required_arr; - - calculator_tool.inputSchema = mcp::make_optional(schema); - - std::cout << " Created Tool: " << calculator_tool.name << std::endl; - if (calculator_tool.description.has_value()) { - std::cout << " Description: " << calculator_tool.description.value() - << std::endl; - } - std::cout << std::endl; - - // Create a Resource definition - Resource sample_resource; - sample_resource.uri = "file:///example/data.json"; - sample_resource.name = "Example Data"; - sample_resource.description = - mcp::make_optional(std::string("Sample JSON data resource for testing")); - sample_resource.mimeType = mcp::make_optional(std::string("application/json")); - - std::cout << "3. MCP Resource:" << std::endl; - std::cout << " URI: " << sample_resource.uri << std::endl; - std::cout << " Name: " << sample_resource.name << std::endl; - if (sample_resource.mimeType.has_value()) { - std::cout << " MIME Type: " << sample_resource.mimeType.value() - << std::endl; - } - std::cout << std::endl; - - // Create a Prompt definition - Prompt greeting_prompt; - greeting_prompt.name = "greeting"; - greeting_prompt.description = - mcp::make_optional(std::string("A simple greeting prompt")); - - PromptArgument name_arg; - name_arg.name = "name"; - name_arg.description = mcp::make_optional(std::string("The name to greet")); - name_arg.required = true; - - greeting_prompt.arguments = - mcp::make_optional(std::vector{name_arg}); - - std::cout << "4. MCP Prompt:" << std::endl; - std::cout << " Name: " << greeting_prompt.name << std::endl; - if (greeting_prompt.description.has_value()) { - std::cout << " Description: " << greeting_prompt.description.value() - << std::endl; - } - if (greeting_prompt.arguments.has_value()) { - std::cout << " Arguments: " << greeting_prompt.arguments.value().size() - << std::endl; - for (const auto& arg : greeting_prompt.arguments.value()) { - std::cout << " - " << arg.name; - if (arg.required) { - std::cout << " (required)"; - } - std::cout << std::endl; - } - } - std::cout << std::endl; - - // Demonstrate JSON serialization - std::cout << "5. JSON Operations:" << std::endl; - json::JsonValue data; - data["greeting"] = hello.greet(); - data["version"] = Version::string(); - data["tool_count"] = 1; - data["resource_count"] = 1; - - std::cout << " Created JSON object with greeting and version info" - << std::endl; - std::cout << std::endl; - - // Integration example: Using gopher-orch to enhance gopher-mcp - std::cout << "6. Integration Example:" << std::endl; - HelloBuilder builder; - auto orchestrator = builder.with_name("MCP Orchestrator").build(); - std::cout << " " << orchestrator->greet() << std::endl; - std::cout - << " This demonstrates gopher-orch extending gopher-mcp capabilities" - << std::endl; - std::cout << std::endl; - - std::cout << "=== Example Complete ===" << std::endl; - std::cout - << "The gopher-orch library successfully integrates with gopher-mcp!" - << std::endl; - - return 0; -} diff --git a/third_party/gopher-orch/examples/multi_agent/README.md b/third_party/gopher-orch/examples/multi_agent/README.md deleted file mode 100644 index 7c6d8193..00000000 --- a/third_party/gopher-orch/examples/multi_agent/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# Multi-Agent Coordination Example - -Demonstrates multiple specialized agents working together on a complex task. - -## What This Example Shows - -- Creating specialized agents with different tools -- Sequential agent coordination -- Passing data between agents -- Building a research-analyze-write pipeline - -## Running - -```bash -# Build -cd build -make multi_agent - -# Run (requires OpenAI API key) -OPENAI_API_KEY=sk-... ./bin/multi_agent -``` - -## Expected Output - -``` -Multi-Agent Coordination Demo -======================================== - -Topic: AI adoption trends in enterprise ----------------------------------------- - -[Phase 1] Research Agent gathering information... - Research complete. - -[Phase 2] Analyzer Agent processing data... - Analysis complete. - -[Phase 3] Writer Agent generating report... - Report generated. - -======================================== -FINAL REPORT: -======================================== -# AI Adoption Trends in Enterprise - -Based on our research and analysis, here are the key findings... - -======================================== -Multi-agent workflow complete. -``` - -## Agent Architecture - -``` - ┌─────────────────┐ - │ Coordinator │ - └────────┬────────┘ - │ - ┌───────────────────┼───────────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Researcher │ │ Analyzer │ │ Writer │ -│ │ │ │ │ │ -│ Tools: │ │ Tools: │ │ Tools: │ -│ - search_web │ │ - calc_stats │ │ - format_report │ -│ - fetch_data │ │ - id_trends │ │ │ -└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ - │ │ │ - └───────► Data ─────┴───────► Output ───┘ -``` - -## Code Walkthrough - -### 1. Create Specialized Agent -```cpp -auto researcher = createSpecializedAgent( - provider, - "Researcher", - "You are a research specialist. Your job is to gather information " - "using search and data fetching tools.", - researchTools); -``` - -### 2. Agent-Specific Tools -```cpp -auto researchTools = makeToolRegistry(); -researchTools->addSyncTool( - "search_web", - "Search the web for information", - JsonValue::object(), - [](const JsonValue& args) -> Result { - // Search implementation - }); -``` - -### 3. Sequential Coordination -```cpp -// Phase 1: Research -researcher->invoke(researchQuery, config, dispatcher, - [&researchResult](Result result) { - researchResult = mcp::get(result); - }); - -// Phase 2: Analysis (uses research results) -JsonValue analysisInput; -analysisInput["research"] = researchResult; -analyzer->invoke(analysisInput, config, dispatcher, callback); - -// Phase 3: Writing (uses both research and analysis) -JsonValue writerInput; -writerInput["research"] = researchResult; -writerInput["analysis"] = analysisResult; -writer->invoke(writerInput, config, dispatcher, callback); -``` - -## Agent Roles - -| Agent | Purpose | Tools | -|-------|---------|-------| -| Researcher | Gather information | search_web, fetch_data | -| Analyzer | Process and analyze data | calculate_stats, identify_trends | -| Writer | Generate reports | format_report | - -## Coordination Patterns - -### Sequential Pipeline -``` -Researcher → Analyzer → Writer -``` -Each agent receives output from previous agents. - -### Parallel Execution (Alternative) -```cpp -// Run research and analysis in parallel -auto parallel = makeParallel({researcher, analyzer}); -parallel->invoke(input, config, dispatcher, callback); -``` - -### Supervisor Pattern (Alternative) -```cpp -// Supervisor decides which agent to call -auto supervisor = makeSupervisorAgent( - {researcher, analyzer, writer}, - supervisorPrompt); -``` - -## Key Concepts - -- **Specialization**: Each agent has focused capabilities -- **Tool Isolation**: Agents only access their own tools -- **Data Flow**: Results passed between agents -- **Coordination**: Sequential or parallel execution - -## See Also - -- [Agent Framework](../../docs/Agent.md) -- [Composition Patterns](../../docs/Composition.md) -- [Simple Agent Example](../simple_agent/) diff --git a/third_party/gopher-orch/examples/multi_agent/main.cc b/third_party/gopher-orch/examples/multi_agent/main.cc deleted file mode 100644 index 1c3f1182..00000000 --- a/third_party/gopher-orch/examples/multi_agent/main.cc +++ /dev/null @@ -1,257 +0,0 @@ -// Multi-Agent Coordination Example -// -// Demonstrates multiple specialized agents working together: -// - Researcher agent: Gathers information -// - Analyzer agent: Analyzes data -// - Writer agent: Generates reports -// - Coordinator: Orchestrates the workflow - -#include -#include - -#include "gopher/orch/orch.h" - -using namespace gopher::orch; -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -// Agent task result -struct AgentResult { - std::string agent_name; - std::string output; - int tokens_used; -}; - -// Create a specialized agent with specific tools and prompt -AgentRunnablePtr createSpecializedAgent(LLMProviderPtr provider, - const std::string& name, - const std::string& system_prompt, - ToolRegistryPtr tools) { - return AgentRunnable::create(provider, makeToolExecutor(tools), - AgentConfig("gpt-4") - .withSystemPrompt(system_prompt) - .withMaxIterations(3)); -} - -int main() { - const char* api_key = std::getenv("OPENAI_API_KEY"); - if (!api_key) { - std::cerr << "Error: OPENAI_API_KEY environment variable not set\n"; - return 1; - } - - auto dispatcher = mcp::event::createLibeventDispatcher(); - auto provider = makeOpenAIProvider(api_key, "gpt-4"); - - std::cout << "Multi-Agent Coordination Demo\n"; - std::cout << "========================================\n\n"; - - // ========================================================================= - // Create specialized agents with their tools - // ========================================================================= - - // 1. Researcher Agent - gathers information - auto researchTools = makeToolRegistry(); - researchTools->addSyncTool( - "search_web", - "Search the web for information. Input: {\"query\": \"...\"}", - JsonValue::object(), [](const JsonValue& args) -> Result { - auto query = args["query"].getString(); - JsonValue results = JsonValue::object(); - results["query"] = query; - results["findings"] = JsonValue::array({ - JsonValue("Finding 1: " + query + " shows positive trends"), - JsonValue("Finding 2: Market data indicates growth"), - JsonValue("Finding 3: Expert opinions are mixed"), - }); - return makeSuccess(std::move(results)); - }); - - researchTools->addSyncTool( - "fetch_data", "Fetch data from a source. Input: {\"source\": \"...\"}", - JsonValue::object(), [](const JsonValue& args) -> Result { - auto source = args["source"].getString(); - JsonValue data = JsonValue::object(); - data["source"] = source; - data["data"] = JsonValue::array({ - JsonValue(42.5), - JsonValue(38.2), - JsonValue(45.8), - JsonValue(51.3), - }); - return makeSuccess(std::move(data)); - }); - - auto researcher = createSpecializedAgent( - provider, "Researcher", - "You are a research specialist. Your job is to gather information " - "using search and data fetching tools. Be thorough and systematic.", - researchTools); - - // 2. Analyzer Agent - analyzes data - auto analyzerTools = makeToolRegistry(); - analyzerTools->addSyncTool( - "calculate_stats", - "Calculate statistics on data. Input: {\"values\": [...]}", - JsonValue::object(), [](const JsonValue& args) -> Result { - auto& values = args["values"]; - double sum = 0; - double min = 1e9, max = -1e9; - int count = 0; - - for (size_t i = 0; i < values.size(); i++) { - double val = values[i].getFloat(); - sum += val; - if (val < min) - min = val; - if (val > max) - max = val; - count++; - } - - JsonValue stats = JsonValue::object(); - stats["count"] = count; - stats["sum"] = sum; - stats["average"] = count > 0 ? sum / count : 0; - stats["min"] = min; - stats["max"] = max; - return makeSuccess(std::move(stats)); - }); - - analyzerTools->addSyncTool( - "identify_trends", "Identify trends in data. Input: {\"data\": [...]}", - JsonValue::object(), [](const JsonValue& args) -> Result { - JsonValue trends = JsonValue::object(); - trends["trend"] = "upward"; - trends["confidence"] = 0.85; - trends["insight"] = "Data shows consistent growth pattern"; - return makeSuccess(std::move(trends)); - }); - - auto analyzer = createSpecializedAgent( - provider, "Analyzer", - "You are a data analyst. Your job is to analyze data, calculate " - "statistics, and identify trends. Provide clear insights.", - analyzerTools); - - // 3. Writer Agent - generates reports - auto writerTools = makeToolRegistry(); - writerTools->addSyncTool( - "format_report", - "Format content as a report. Input: {\"title\": \"...\", \"sections\": " - "[...]}", - JsonValue::object(), [](const JsonValue& args) -> Result { - std::string report = "# " + args["title"].getString() + "\n\n"; - auto& sections = args["sections"]; - for (size_t i = 0; i < sections.size(); i++) { - report += "## Section " + std::to_string(i + 1) + "\n"; - report += sections[i].getString() + "\n\n"; - } - JsonValue result = JsonValue::object(); - result["report"] = report; - return makeSuccess(std::move(result)); - }); - - auto writer = createSpecializedAgent( - provider, "Writer", - "You are a technical writer. Your job is to create clear, " - "well-structured reports from research and analysis results.", - writerTools); - - // ========================================================================= - // Orchestrate multi-agent workflow - // ========================================================================= - - std::string topic = "AI adoption trends in enterprise"; - - std::cout << "Topic: " << topic << "\n"; - std::cout << "----------------------------------------\n\n"; - - // Step 1: Research Phase - std::cout << "[Phase 1] Research Agent gathering information...\n"; - JsonValue researchResult; - { - bool done = false; - JsonValue input = JsonValue::object(); - input["query"] = "Research: " + topic; - - researcher->invoke(input, RunnableConfig(), *dispatcher, - [&done, &researchResult](Result result) { - if (mcp::holds_alternative(result)) { - std::cerr << "Research failed: " - << mcp::get(result).message << "\n"; - } else { - researchResult = mcp::get(result); - std::cout << " Research complete.\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - // Step 2: Analysis Phase - std::cout << "\n[Phase 2] Analyzer Agent processing data...\n"; - JsonValue analysisResult; - { - bool done = false; - JsonValue input = JsonValue::object(); - input["research"] = researchResult; - input["query"] = "Analyze the research findings"; - - analyzer->invoke(input, RunnableConfig(), *dispatcher, - [&done, &analysisResult](Result result) { - if (mcp::holds_alternative(result)) { - std::cerr << "Analysis failed: " - << mcp::get(result).message << "\n"; - } else { - analysisResult = mcp::get(result); - std::cout << " Analysis complete.\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - // Step 3: Writing Phase - std::cout << "\n[Phase 3] Writer Agent generating report...\n"; - { - bool done = false; - JsonValue input = JsonValue::object(); - input["research"] = researchResult; - input["analysis"] = analysisResult; - input["query"] = "Create a report on: " + topic; - - writer->invoke( - input, RunnableConfig(), *dispatcher, - [&done](Result result) { - if (mcp::holds_alternative(result)) { - std::cerr << "Writing failed: " << mcp::get(result).message - << "\n"; - } else { - auto& output = mcp::get(result); - std::cout << " Report generated.\n\n"; - std::cout << "========================================\n"; - std::cout << "FINAL REPORT:\n"; - std::cout << "========================================\n"; - std::cout << output["response"].getString() << "\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - std::cout << "\n========================================\n"; - std::cout << "Multi-agent workflow complete.\n"; - - return 0; -} diff --git a/third_party/gopher-orch/examples/resilient_api/README.md b/third_party/gopher-orch/examples/resilient_api/README.md deleted file mode 100644 index cc7b4fc0..00000000 --- a/third_party/gopher-orch/examples/resilient_api/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# Resilient API Client Example - -Demonstrates resilience patterns for handling unreliable external services. - -## What This Example Shows - -- Retry with exponential backoff -- Timeout protection -- Fallback on failure -- Circuit breaker for failure isolation -- Combining multiple resilience patterns - -## Running - -```bash -# Build -cd build -make resilient_api - -# Run -./bin/resilient_api -``` - -## Expected Output - -``` -Resilient API Client Demo -======================================== - -1. Retry Pattern (max 3 attempts, exponential backoff) ----------------------------------------- - Success: Response from /api/data - -2. Timeout Pattern (150ms timeout) ----------------------------------------- - Timeout or error: Operation timed out - -3. Fallback Pattern ----------------------------------------- - Got data: Cached fallback data for /api/unreliable - -4. Circuit Breaker Pattern ----------------------------------------- - Call 1: Failed: Connection failed - Call 2: Failed: Connection failed - Call 3: Failed: Connection failed - Call 4: Circuit OPEN - call rejected - Call 5: Circuit OPEN - call rejected - Call 6: Circuit OPEN - call rejected - -5. Combined Resilience (Retry + Timeout + Fallback) ----------------------------------------- - Got data: Response from /api/important - -======================================== -Demo complete. -``` - -## Resilience Patterns - -### 1. Retry with Backoff -```cpp -auto retryConfig = RetryConfig() - .withMaxAttempts(3) - .withInitialDelay(std::chrono::milliseconds(100)) - .withMaxDelay(std::chrono::milliseconds(1000)) - .withBackoffMultiplier(2.0); - -auto retryableApi = makeRetry(apiCall, retryConfig); -``` - -### 2. Timeout Protection -```cpp -auto timedApi = makeTimeout(slowApi, std::chrono::milliseconds(150)); -``` - -### 3. Fallback on Failure -```cpp -auto safeApi = makeFallback(unreliableApi, fallbackApi); -``` - -### 4. Circuit Breaker -```cpp -auto cbConfig = CircuitBreakerConfig() - .withFailureThreshold(3) // Open after 3 failures - .withSuccessThreshold(2) // Close after 2 successes - .withTimeout(std::chrono::seconds(5)); // Half-open after 5s - -auto protectedApi = makeCircuitBreaker(apiCall, cbConfig); -``` - -### 5. Combined Patterns -```cpp -// Build defense-in-depth: retry -> timeout -> fallback -auto combinedApi = makeFallback( - makeTimeout( - makeRetry(apiCall, RetryConfig().withMaxAttempts(2)), - std::chrono::milliseconds(300)), - fallbackApi); -``` - -## Key Concepts - -- **Retry**: Automatically retry failed operations with configurable backoff -- **Timeout**: Bound operation duration to prevent hanging -- **Fallback**: Provide degraded response when primary fails -- **Circuit Breaker**: Stop calling failing services to allow recovery - -## Circuit Breaker States - -``` - ┌─────────────────────────────────────┐ - │ │ - ▼ │ - CLOSED ──(failures >= threshold)──► OPEN - ▲ │ - │ │ - │ (timeout expires) - │ │ - │ ▼ - └───(successes >= threshold)─── HALF_OPEN -``` - -## See Also - -- [Resilience Patterns](../../docs/Resilience.md) -- [Runnable Interface](../../docs/Runnable.md) diff --git a/third_party/gopher-orch/examples/resilient_api/main.cc b/third_party/gopher-orch/examples/resilient_api/main.cc deleted file mode 100644 index 9a9c1630..00000000 --- a/third_party/gopher-orch/examples/resilient_api/main.cc +++ /dev/null @@ -1,277 +0,0 @@ -// Resilient API Client Example -// -// Demonstrates resilience patterns for external API calls: -// - Retry with exponential backoff -// - Timeout protection -// - Fallback on failure -// - Circuit breaker for failure isolation - -#include -#include -#include - -#include "gopher/orch/orch.h" - -using namespace gopher::orch; -using namespace gopher::orch::core; -using namespace gopher::orch::resilience; - -// Simulated API response -struct ApiResponse { - bool success; - std::string data; - int latency_ms; -}; - -// Simulated unreliable API client -class UnreliableApiClient { - public: - UnreliableApiClient(double failure_rate = 0.5, int max_latency_ms = 500) - : failure_rate_(failure_rate), - max_latency_ms_(max_latency_ms), - gen_(std::random_device{}()) {} - - // Simulates an API call that may fail or be slow - void fetch(const std::string& endpoint, - Dispatcher& dispatcher, - std::function)> callback) { - std::uniform_real_distribution<> fail_dist(0.0, 1.0); - std::uniform_int_distribution<> latency_dist(10, max_latency_ms_); - - bool will_fail = fail_dist(gen_) < failure_rate_; - int latency = latency_dist(gen_); - - // Simulate network latency - dispatcher.setTimeout( - [this, endpoint, will_fail, latency, callback = std::move(callback)]() { - if (will_fail) { - callback(makeOrchError( - OrchError::NETWORK_ERROR, "Connection failed to " + endpoint)); - } else { - ApiResponse response; - response.success = true; - response.data = "Response from " + endpoint; - response.latency_ms = latency; - callback(makeSuccess(std::move(response))); - } - }, - std::chrono::milliseconds(latency)); - } - - void setFailureRate(double rate) { failure_rate_ = rate; } - - private: - double failure_rate_; - int max_latency_ms_; - std::mt19937 gen_; -}; - -// Create a runnable from the API client -RunnablePtr makeApiRunnable( - std::shared_ptr client) { - return makeLambda( - [client](const std::string& endpoint, Dispatcher& dispatcher, - ResultCallback callback) { - client->fetch(endpoint, dispatcher, std::move(callback)); - }); -} - -int main() { - auto dispatcher = mcp::event::createLibeventDispatcher(); - - // Create unreliable API client (50% failure rate) - auto client = std::make_shared(0.5, 200); - auto apiCall = makeApiRunnable(client); - - std::cout << "Resilient API Client Demo\n"; - std::cout << "========================================\n\n"; - - // ========================================================================= - // Pattern 1: Retry with Exponential Backoff - // ========================================================================= - std::cout << "1. Retry Pattern (max 3 attempts, exponential backoff)\n"; - std::cout << "----------------------------------------\n"; - - auto retryConfig = RetryConfig() - .withMaxAttempts(3) - .withInitialDelay(std::chrono::milliseconds(100)) - .withMaxDelay(std::chrono::milliseconds(1000)) - .withBackoffMultiplier(2.0); - - auto retryableApi = makeRetry(apiCall, retryConfig); - - { - bool done = false; - int attempt = 0; - retryableApi->invoke( - "/api/data", RunnableConfig(), *dispatcher, - [&done, &attempt](Result result) { - if (mcp::holds_alternative(result)) { - std::cout << " Failed after retries: " - << mcp::get(result).message << "\n"; - } else { - auto& response = mcp::get(result); - std::cout << " Success: " << response.data << "\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - // ========================================================================= - // Pattern 2: Timeout Protection - // ========================================================================= - std::cout << "\n2. Timeout Pattern (150ms timeout)\n"; - std::cout << "----------------------------------------\n"; - - // Create slow API (high latency) - auto slowClient = std::make_shared(0.0, 500); - auto slowApi = makeApiRunnable(slowClient); - auto timedApi = makeTimeout(slowApi, std::chrono::milliseconds(150)); - - { - bool done = false; - timedApi->invoke("/api/slow", RunnableConfig(), *dispatcher, - [&done](Result result) { - if (mcp::holds_alternative(result)) { - std::cout << " Timeout or error: " - << mcp::get(result).message << "\n"; - } else { - auto& response = mcp::get(result); - std::cout - << " Success (within timeout): " << response.data - << "\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - // ========================================================================= - // Pattern 3: Fallback on Failure - // ========================================================================= - std::cout << "\n3. Fallback Pattern\n"; - std::cout << "----------------------------------------\n"; - - // Create always-failing API - auto failingClient = std::make_shared(1.0, 50); - auto failingApi = makeApiRunnable(failingClient); - - // Create fallback that returns cached data - auto fallbackApi = makeLambda( - [](const std::string& endpoint, Dispatcher& dispatcher, - ResultCallback callback) { - ApiResponse cached; - cached.success = true; - cached.data = "Cached fallback data for " + endpoint; - cached.latency_ms = 0; - callback(makeSuccess(std::move(cached))); - }); - - auto safeApi = makeFallback(failingApi, fallbackApi); - - { - bool done = false; - safeApi->invoke( - "/api/unreliable", RunnableConfig(), *dispatcher, - [&done](Result result) { - if (mcp::holds_alternative(result)) { - std::cout << " Error: " << mcp::get(result).message << "\n"; - } else { - auto& response = mcp::get(result); - std::cout << " Got data: " << response.data << "\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - // ========================================================================= - // Pattern 4: Circuit Breaker - // ========================================================================= - std::cout << "\n4. Circuit Breaker Pattern\n"; - std::cout << "----------------------------------------\n"; - - auto cbConfig = CircuitBreakerConfig() - .withFailureThreshold(3) - .withSuccessThreshold(2) - .withTimeout(std::chrono::seconds(5)); - - // Reset client to 70% failure rate for circuit breaker demo - client->setFailureRate(0.7); - auto protectedApi = makeCircuitBreaker(apiCall, cbConfig); - - // Make multiple calls to trigger circuit breaker - for (int i = 1; i <= 6; i++) { - bool done = false; - std::cout << " Call " << i << ": "; - - protectedApi->invoke( - "/api/fragile", RunnableConfig(), *dispatcher, - [&done](Result result) { - if (mcp::holds_alternative(result)) { - const auto& err = mcp::get(result); - if (err.message.find("Circuit open") != std::string::npos) { - std::cout << "Circuit OPEN - call rejected\n"; - } else { - std::cout << "Failed: " << err.message << "\n"; - } - } else { - std::cout << "Success\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - // ========================================================================= - // Pattern 5: Combined Resilience - // ========================================================================= - std::cout << "\n5. Combined Resilience (Retry + Timeout + Fallback)\n"; - std::cout << "----------------------------------------\n"; - - // Reset client for combined demo - client->setFailureRate(0.3); - - auto combinedApi = makeFallback( - makeTimeout(makeRetry(apiCall, RetryConfig().withMaxAttempts(2)), - std::chrono::milliseconds(300)), - fallbackApi); - - { - bool done = false; - combinedApi->invoke( - "/api/important", RunnableConfig(), *dispatcher, - [&done](Result result) { - if (mcp::holds_alternative(result)) { - std::cout << " Final error: " << mcp::get(result).message - << "\n"; - } else { - auto& response = mcp::get(result); - std::cout << " Got data: " << response.data << "\n"; - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - std::cout << "\n========================================\n"; - std::cout << "Demo complete.\n"; - - return 0; -} diff --git a/third_party/gopher-orch/examples/sdk/CMakeLists.txt b/third_party/gopher-orch/examples/sdk/CMakeLists.txt deleted file mode 100644 index f47c113e..00000000 --- a/third_party/gopher-orch/examples/sdk/CMakeLists.txt +++ /dev/null @@ -1,113 +0,0 @@ -# SDK Examples - -# Client example demonstrating ToolsFetcher usage -add_executable(client_example client_example.cpp) -target_link_libraries(client_example - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(client_example PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) - -# JSON client example using the static Agent::run() method with JSON config -add_executable(client_example_json client_example_json.cpp) -target_link_libraries(client_example_json - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(client_example_json PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) - -# API client example demonstrating remote API integration -add_executable(client_example_api client_example_api.cpp) -target_link_libraries(client_example_api - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(client_example_api PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) - -# Test for error response handling -add_executable(test_error_response test_error_response.cpp) -target_link_libraries(test_error_response - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(test_error_response PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) - -# Gateway server example - MCP server aggregating multiple backend servers -add_executable(gateway_server_example gateway_server_example.cpp) -target_link_libraries(gateway_server_example - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(gateway_server_example PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) - -# Gateway server test client - connects to gateway_server_example -add_executable(gateway_server_example_test gateway_server_example_test.cpp) -target_link_libraries(gateway_server_example_test - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(gateway_server_example_test PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) - -# JSON client example for listing tools from MCP servers -add_executable(client_example_json_list_tool client_example_json_list_tool.cpp) -target_link_libraries(client_example_json_list_tool - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(client_example_json_list_tool PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) - -# JSON client example for listing tools using ReActAgent -add_executable(client_example_json_list_tool_agent client_example_json_list_tool_agent.cpp) -target_link_libraries(client_example_json_list_tool_agent - gopher-orch - ${GOPHER_MCP_LIBRARIES} - Threads::Threads -) - -# Set runtime path for finding shared libraries -set_target_properties(client_example_json_list_tool_agent PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples/sdk" - INSTALL_RPATH_USE_LINK_PATH TRUE -) \ No newline at end of file diff --git a/third_party/gopher-orch/examples/sdk/client_example.cpp b/third_party/gopher-orch/examples/sdk/client_example.cpp deleted file mode 100644 index 899b763b..00000000 --- a/third_party/gopher-orch/examples/sdk/client_example.cpp +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @file client_example.cpp - * @brief Basic MCP client example with ReActAgent - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "mcp/event/libevent_dispatcher.h" -#include "gopher/orch/agent/tools_fetcher.h" -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/agent/agent.h" -#include "gopher/orch/llm/anthropic_provider.h" - -using namespace gopher::orch; -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; - -int main(int argc, char* argv[]) { - std::cout << "=== ToolsFetcher Basic Example ===" << std::endl; - std::cout << "Usage: " << argv[0] << " [query]" << std::endl; - std::cout << "Example: " << argv[0] << " \"What is the weather in New York?\"" << std::endl; - std::cout << "Default query: \"What tools are available?\"" << std::endl << std::endl; - - auto dispatcher = std::make_unique("client_example"); - - // MCP server configuration - std::string json_config = R"({ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "server1", - "transport": "http_sse", - "config": { - "url": "http://127.0.0.1:3001/rpc", - "headers": {} - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - }, - { - "version": "2025-01-09", - "serverId": "1877234567890123457", - "name": "server2", - "transport": "http_sse", - "config": { - "url": "http://127.0.0.1:3002/rpc", - "headers": {} - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - } - ] - } - })"; - - auto tools_fetcher = std::make_unique(); - - std::cout << "Loading MCP server configuration..." << std::endl; - - bool load_complete = false; - bool load_success = false; - - tools_fetcher->loadFromJson(json_config, *dispatcher, - [&load_complete, &load_success](VoidResult result) { - std::cout << "loadFromJson callback received" << std::endl; - load_complete = true; - if (mcp::holds_alternative(result)) { - load_success = true; - std::cout << "Configuration loaded successfully!" << std::endl; - } else { - std::cerr << "Failed to load configuration: " - << mcp::get(result).message << std::endl; - } - }); - - // Wait for loading to complete - auto load_start = std::chrono::steady_clock::now(); - auto load_timeout = std::chrono::seconds(10); - - while (!load_complete) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (std::chrono::steady_clock::now() - load_start > load_timeout) { - std::cerr << "Configuration loading timed out after 10 seconds." << std::endl; - std::cerr << "This usually means the MCP servers are not responding." << std::endl; - return 1; - } - } - - if (!load_success) { - std::cerr << "\nConfiguration failed. Common causes:" << std::endl; - std::cerr << "1. No MCP servers running at the configured URLs" << std::endl; - std::cerr << "2. Servers are not MCP-compliant (need SSE transport)" << std::endl; - std::cerr << "3. Network/firewall issues" << std::endl; - std::cerr << "\nTo run MCP servers, try:" << std::endl; - std::cerr << " npx @modelcontextprotocol/server-everything --port 3001" << std::endl; - - std::cerr << "Exiting due to connection failure." << std::endl; - _exit(1); - } - - // Allow async operations to complete - for (int i = 0; i < 10; i++) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - auto registry = tools_fetcher->getRegistry(); - if (!registry) { - std::cerr << "Failed to get ToolRegistry!" << std::endl; - return 1; - } - - std::cout << "Discovered " << registry->toolCount() << " tools" << std::endl; - - // Show available tools - auto tool_specs = registry->getToolSpecs(); - std::cout << "\nAvailable tools:" << std::endl; - for (const auto& spec : tool_specs) { - std::cout << " - " << spec.name << ": " << spec.description << std::endl; - std::cout << " Parameters: " << spec.parameters.toString() << std::endl; - } - - if (registry->toolCount() == 0) { - std::cerr << "\nNo tools discovered. Please ensure MCP servers are running." << std::endl; - return 1; - } - - // Setup LLM provider - const char* api_key = std::getenv("ANTHROPIC_API_KEY"); - if (!api_key || std::strlen(api_key) == 0) { - std::cerr << "ANTHROPIC_API_KEY environment variable not set!" << std::endl; - return 1; - } - auto llm_provider = AnthropicProvider::create(api_key); - - // Create agent - AgentConfig agent_config("claude-3-haiku-20240307"); - agent_config.withSystemPrompt( - "You are a helpful assistant with access to various tools. " - "Use the appropriate tools to complete tasks. " - "Always explain your reasoning before taking action."); - agent_config.withMaxIterations(5); - agent_config.withTemperature(0.3); - - auto agent = ReActAgent::create(llm_provider, registry, agent_config); - if (!agent) { - std::cerr << "Failed to create ReActAgent!" << std::endl; - return 1; - } - - // Get query - std::string query = "What time is it in Tokyo?"; - if (argc > 1) { - query = ""; - for (int i = 1; i < argc; i++) { - if (i > 1) query += " "; - query += argv[i]; - } - } - std::cout << "\nQuery: " << query << std::endl; - std::cout << "Running agent..." << std::endl; - - std::promise completion_promise; - auto completion_future = completion_promise.get_future(); - - agent->run(query, *dispatcher, - [&completion_promise](Result result) { - if (mcp::holds_alternative(result)) { - auto response = mcp::get(result); - std::cout << "\nAgent Response:" << std::endl; - std::cout << "--------------------------------" << std::endl; - std::cout << "\n" << response.response << std::endl; - std::cout << "\n--------------------------------" << std::endl; - std::cout << "Total steps: " << response.iterationCount() << std::endl; - std::cout << "Status: " << agentStatusToString(response.status) << std::endl; - if (response.total_usage.total_tokens > 0) { - std::cout << "Tokens used: " << response.total_usage.total_tokens << std::endl; - } - completion_promise.set_value(0); - } else { - std::cerr << "Agent error: " - << mcp::get(result).message << std::endl; - completion_promise.set_value(0); - } - }); - - // Run with timeout - auto start_time = std::chrono::steady_clock::now(); - auto timeout_duration = std::chrono::seconds(30); - - while (completion_future.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) { - if (std::chrono::steady_clock::now() - start_time > timeout_duration) { - std::cerr << "Query timed out!" << std::endl; - std::cout << "\n=== Example Complete (Timeout) ===" << std::endl; - _exit(1); - } - - dispatcher->run(mcp::event::RunType::NonBlock); - } - - std::cout << "\n=== Example Complete ===" << std::endl; - exit(0); -} \ No newline at end of file diff --git a/third_party/gopher-orch/examples/sdk/client_example_api.cpp b/third_party/gopher-orch/examples/sdk/client_example_api.cpp deleted file mode 100644 index afe1bdcd..00000000 --- a/third_party/gopher-orch/examples/sdk/client_example_api.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @file client_example_api.cpp - * @brief Simple MCP client example that fetches server configurations from remote API - */ - -#include -#include - -#include "gopher/orch/agent/agent.h" - -using namespace gopher::orch::agent; - -int main(int argc, char* argv[]) { - std::cout << "=== Remote API MCP Client Example ===" << std::endl; - std::cout << "Usage: " << argv[0] << " [query1] [query2] ..." << std::endl; - std::cout << "Example: " << argv[0] << " \"What tools are available?\" \"What time is it in Tokyo?\"" << std::endl; - std::cout << "Default queries if none provided:" << std::endl; - std::cout << " 1. What tools are available?" << std::endl; - std::cout << " 2. What time is it in Tokyo?" << std::endl << std::endl; - - // Parse queries from command line arguments - std::vector queries; - if (argc > 1) { - for (int i = 1; i < argc; i++) { - queries.push_back(argv[i]); - } - } else { - // Default queries if none provided - queries.push_back("What tools are available?"); - queries.push_back("What time is it in Tokyo?"); - } - - std::string provider = "AnthropicProvider"; - std::string model = "claude-3-haiku-20240307"; - std::string apiKey = "sk_xkmdfiw3jfndeaypegwb"; - - std::cout << "Provider: " << provider << std::endl; - std::cout << "Model: " << model << std::endl; - std::cout << "apiKey: " << apiKey << std::endl; - std::cout << "Number of queries: " << queries.size() << std::endl; - std::cout << "Creating agent with API key..." << std::endl; - - auto agent = ReActAgent::createByApiKey(provider, model, apiKey); - if (!agent) { - std::cout << "Error: Failed to create agent" << std::endl; - return 1; - } - std::cout << "Agent created successfully!" << std::endl; - - // Execute all queries - for (size_t i = 0; i < queries.size(); i++) { - std::cout << "\nQuery " << (i + 1) << ": " << queries[i] << std::endl; - - std::string answer = agent->run(queries[i]); - std::cout << "\nAgent Response " << (i + 1) << ":" << std::endl; - std::cout << "--------------------------------" << std::endl; - std::cout << answer << std::endl; - std::cout << "--------------------------------" << std::endl; - } - - return 0; -} \ No newline at end of file diff --git a/third_party/gopher-orch/examples/sdk/client_example_json.cpp b/third_party/gopher-orch/examples/sdk/client_example_json.cpp deleted file mode 100644 index d541e66c..00000000 --- a/third_party/gopher-orch/examples/sdk/client_example_json.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @file client_example_json.cpp - * @brief MCP client example using JSON config with the static Agent::run() method - */ - -#include -#include - -#include "gopher/orch/agent/agent.h" - -using namespace gopher::orch::agent; - -int main(int argc, char* argv[]) { - std::cout << "=== Simple Agent Example ===" << std::endl; - std::cout << "Usage: " << argv[0] << " [query1] [query2] [query3] ..." << std::endl; - std::cout << "Example: " << argv[0] << " \"What time is it in Tokyo?\" \"Generate a 12-character password\"" << std::endl; - std::cout << "Default queries if none provided:" << std::endl; - std::cout << " 1. What time is it in Tokyo?" << std::endl; - std::cout << " 2. Generate a 12-character password" << std::endl << std::endl; - - std::string provider = "AnthropicProvider"; - std::string model = "claude-3-haiku-20240307"; - std::string serverJson = R"({ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "gopher-auth-server", - "transport": "http_sse", - "config": { - "url": "http://127.0.0.1:3001/rpc", - "headers": {} - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - }, - { - "version": "2025-01-09", - "serverId": "1877234567890123457", - "name": "gopher-auth-server2", - "transport": "http_sse", - "config": { - "url": "http://127.0.0.1:3002/rpc", - "headers": {} - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - } - ] - } - })"; - - // Parse queries from command line arguments - std::vector queries; - if (argc > 1) { - // Use provided queries - for (int i = 1; i < argc; i++) { - queries.push_back(argv[i]); - } - } else { - // Default queries if none provided - queries.push_back("What time is it in Tokyo?"); - queries.push_back("Generate a 12-character password"); - } - - std::cout << "Provider: " << provider << std::endl; - std::cout << "Model: " << model << std::endl; - std::cout << "Number of queries: " << queries.size() << std::endl; - std::cout << "Creating agent..." << std::endl; - - auto agent = ReActAgent::createByJson(provider, model, serverJson); - if (!agent) { - std::cout << "Error: Failed to create agent" << std::endl; - return 1; - } - - std::cout << "Agent created successfully!" << std::endl; - - // Execute all queries - for (size_t i = 0; i < queries.size(); i++) { - std::cout << "\nQuery " << (i + 1) << ": " << queries[i] << std::endl; - - std::string answer = agent->run(queries[i]); - std::cout << "\nAgent Response " << (i + 1) << ":" << std::endl; - std::cout << "--------------------------------" << std::endl; - std::cout << answer << std::endl; - std::cout << "--------------------------------" << std::endl; - } - - return 0; -} \ No newline at end of file diff --git a/third_party/gopher-orch/examples/sdk/client_example_json_list_tool.cpp b/third_party/gopher-orch/examples/sdk/client_example_json_list_tool.cpp deleted file mode 100644 index 63d4bffd..00000000 --- a/third_party/gopher-orch/examples/sdk/client_example_json_list_tool.cpp +++ /dev/null @@ -1,154 +0,0 @@ -/** - * @file client_example_json_list_tool.cpp - * @brief Simple MCP client example that lists tools from MCP servers - * - * This example demonstrates: - * - Loading MCP server configuration from JSON - * - Connecting to MCP servers via HTTP/SSE transport - * - Fetching and displaying available tools - */ - -#include -#include -#include -#include - -#include "mcp/event/libevent_dispatcher.h" -#include "gopher/orch/agent/tools_fetcher.h" -#include "gopher/orch/agent/tool_registry.h" - -using namespace gopher::orch; -using namespace gopher::orch::agent; - -int main() { - std::cout << "=== MCP Server Tools Listing Example ===" << std::endl; - std::cout << std::endl; - - // Create event dispatcher - auto dispatcher = std::make_unique("list_tools_example"); - - // MCP server configuration JSON - // Note: For HTTPS servers, the URL should start with https:// - // The client will automatically use SSL/TLS for HTTPS connections - std::string serverJson = R"({ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "gopher-auth-server", - "transport": "http_sse", - "config": { - "url": "https://gmail-mcp-434541420901175298.mcp-test.gopher.security/sse", - "headers": {} - }, - "connectTimeout": 10000, - "requestTimeout": 60000 - } - ] - } - })"; - - // Create ToolsFetcher to load configuration - auto tools_fetcher = std::make_unique(); - - std::cout << "Loading MCP server configuration..." << std::endl; - - bool load_complete = false; - bool load_success = false; - - // Load from JSON configuration - tools_fetcher->loadFromJson(serverJson, *dispatcher, - [&load_complete, &load_success](VoidResult result) { - load_complete = true; - if (mcp::holds_alternative(result)) { - load_success = true; - std::cout << "Configuration loaded successfully!" << std::endl; - } else { - std::cerr << "Failed to load configuration: " - << mcp::get(result).message << std::endl; - } - }); - - // Wait for loading to complete with timeout - auto load_start = std::chrono::steady_clock::now(); - auto load_timeout = std::chrono::seconds(15); - - while (!load_complete) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (std::chrono::steady_clock::now() - load_start > load_timeout) { - std::cerr << "Configuration loading timed out after 15 seconds." << std::endl; - std::cerr << "This usually means the MCP server is not responding." << std::endl; - return 1; - } - } - - if (!load_success) { - std::cerr << "\nConfiguration failed. Possible causes:" << std::endl; - std::cerr << "1. MCP server not running at the configured URL" << std::endl; - std::cerr << "2. Network/firewall issues" << std::endl; - std::cerr << "3. Invalid server configuration" << std::endl; - return 1; - } - - // Allow async operations to complete - for (int i = 0; i < 20; i++) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - - // Get the tool registry - auto registry = tools_fetcher->getRegistry(); - if (!registry) { - std::cerr << "Failed to get ToolRegistry!" << std::endl; - return 1; - } - - // Display tool count - size_t tool_count = registry->toolCount(); - std::cout << std::endl; - std::cout << "========================================" << std::endl; - std::cout << "Discovered " << tool_count << " tool(s)" << std::endl; - std::cout << "========================================" << std::endl; - - if (tool_count == 0) { - std::cout << "No tools discovered from the MCP server." << std::endl; - return 0; - } - - // Get and display all tool specifications - auto tool_specs = registry->getToolSpecs(); - - std::cout << std::endl; - for (size_t i = 0; i < tool_specs.size(); i++) { - const auto& spec = tool_specs[i]; - std::cout << "[" << (i + 1) << "] " << spec.name << std::endl; - std::cout << " Description: " << spec.description << std::endl; - std::cout << " Parameters: " << spec.parameters.toString() << std::endl; - std::cout << std::endl; - } - - std::cout << "========================================" << std::endl; - std::cout << "=== Tool Listing Complete ===" << std::endl; - - // Shutdown the tools fetcher to close SSE connections - // SSE connections are long-lived and must be explicitly closed - bool shutdown_complete = false; - tools_fetcher->shutdown(*dispatcher, [&shutdown_complete]() { - shutdown_complete = true; - }); - - // Wait for shutdown to complete - while (!shutdown_complete) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - std::cout << "=== Shutdown Complete ===" << std::endl; - return 0; -} diff --git a/third_party/gopher-orch/examples/sdk/client_example_json_list_tool_agent.cpp b/third_party/gopher-orch/examples/sdk/client_example_json_list_tool_agent.cpp deleted file mode 100644 index fed3ed66..00000000 --- a/third_party/gopher-orch/examples/sdk/client_example_json_list_tool_agent.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @file client_example_json_list_tool_agent.cpp - * @brief Example demonstrating ReActAgent creation and tool listing - * - * This example demonstrates: - * - Creating a ReActAgent using JSON server configuration - * - Accessing the agent's tool registry - * - Displaying available tools and their specifications - */ - -#include -#include -#include - -#include "gopher/orch/agent/agent.h" -#include "gopher/orch/agent/tool_registry.h" - -using namespace gopher::orch::agent; - -int main() { - std::cout << "=== ReActAgent Tool Listing Example ===" << std::endl; - std::cout << std::endl; - - // Configuration - std::string provider = "AnthropicProvider"; - std::string model = "claude-3-haiku-20240307"; - std::string serverJson = R"({ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "gopher-auth-server", - "transport": "http_sse", - "config": { - "url": "https://gmail-mcp-434541420901175298.mcp-test.gopher.security/sse", - "headers": {} - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - } - ] - } - })"; - - std::cout << "Provider: " << provider << std::endl; - std::cout << "Model: " << model << std::endl; - std::cout << std::endl; - - std::cout << "Creating ReActAgent (loading tools from MCP server)..." << std::endl; - - // Create agent - tools are loaded immediately - auto agent = ReActAgent::createByJson(provider, model, serverJson); - - if (!agent) { - std::cerr << "Error: Failed to create agent" << std::endl; - std::cerr << "Possible causes:" << std::endl; - std::cerr << "1. MCP server not running at the configured URL" << std::endl; - std::cerr << "2. Network/firewall issues" << std::endl; - std::cerr << "3. Invalid server configuration" << std::endl; - return 1; - } - - std::cout << "Agent created successfully!" << std::endl; - std::cout << std::endl; - - // Get the tool registry from the agent - auto registry = agent->tools(); - - if (!registry) { - std::cerr << "Error: Failed to get tool registry from agent" << std::endl; - return 1; - } - - // Display tool count - size_t tool_count = registry->toolCount(); - std::cout << "========================================" << std::endl; - std::cout << "Discovered " << tool_count << " tool(s)" << std::endl; - std::cout << "========================================" << std::endl; - - if (tool_count == 0) { - std::cout << "No tools discovered from the MCP server." << std::endl; - } else { - // Get and display all tool specifications - auto tool_specs = registry->getToolSpecs(); - - std::cout << std::endl; - for (size_t i = 0; i < tool_specs.size(); i++) { - const auto& spec = tool_specs[i]; - std::cout << "[" << (i + 1) << "] " << spec.name << std::endl; - std::cout << " Description: " << spec.description << std::endl; - std::cout << " Parameters: " << spec.parameters.toString() << std::endl; - std::cout << std::endl; - } - } - - std::cout << "========================================" << std::endl; - std::cout << "=== Tool Listing Complete ===" << std::endl; - - return 0; -} diff --git a/third_party/gopher-orch/examples/sdk/gateway_server_example.cpp b/third_party/gopher-orch/examples/sdk/gateway_server_example.cpp deleted file mode 100644 index 6044341f..00000000 --- a/third_party/gopher-orch/examples/sdk/gateway_server_example.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @file gateway_server_example.cpp - * @brief Example demonstrating GatewayServer that aggregates multiple MCP servers - * - * This example creates a GatewayServer that: - * 1. Connects to multiple backend MCP servers - * 2. Discovers their tools - * 3. Exposes all tools via a single MCP server endpoint - * - * Usage: - * ./gateway_server_example - * - * Prerequisites: - * - MCP servers running on localhost:3001 and localhost:3002 - * - * The gateway will start on localhost:3003 and expose all tools - * from both backend servers. - */ - -#include - -#include "gopher/orch/server/gateway_server.h" - -using namespace gopher::orch::server; - -int main(int argc, char* argv[]) { - std::cout << "=== GatewayServer Example ===" << std::endl; - std::cout << std::endl; - - // JSON configuration for backend servers - // This format matches the API response format - std::string serverJson = R"({ - "succeeded": true, - "data": { - "servers": [ - { - "name": "server3001", - "transport": "http_sse", - "config": { - "url": "http://127.0.0.1:3001/mcp" - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - }, - { - "name": "server3002", - "transport": "http_sse", - "config": { - "url": "http://127.0.0.1:3002/mcp" - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - }, - { - "name": "gmail-server", - "transport": "http_sse", - "config": { - "url": "${some-remote-mcp-server-url}" - }, - "connectTimeout": 20000, - "requestTimeout": 30000 - } - ] - } - })"; - - // Create gateway from JSON configuration - auto gateway = GatewayServer::create(serverJson); - - // Check if creation succeeded - if (!gateway->getError().empty()) { - std::cerr << "Failed to create gateway: " << gateway->getError() << std::endl; - return 1; - } - - // Start listening (blocks until Ctrl+C) - return gateway->listen(3003); -} diff --git a/third_party/gopher-orch/examples/sdk/gateway_server_example_test.cpp b/third_party/gopher-orch/examples/sdk/gateway_server_example_test.cpp deleted file mode 100644 index eae7c2e1..00000000 --- a/third_party/gopher-orch/examples/sdk/gateway_server_example_test.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @file gateway_server_example_test.cpp - * @brief Test client that connects to the GatewayServer to test tool aggregation - * - * This example connects to a running GatewayServer (port 3003) which aggregates - * tools from multiple backend MCP servers. Use this to test the gateway's - * reconnect-on-demand functionality after keep-alive timeouts. - */ - -#include -#include - -#include "gopher/orch/agent/agent.h" - -using namespace gopher::orch::agent; - -int main(int argc, char* argv[]) { - std::cout << "=== Gateway Server Test Client ===" << std::endl; - std::cout << "This client connects to the GatewayServer at port 3003" << std::endl; - std::cout << "Make sure gateway_server_example is running first!" << std::endl; - std::cout << std::endl; - std::cout << "Usage: " << argv[0] << " [query1] [query2] ..." << std::endl; - std::cout << "Example: " << argv[0] << " \"What is the weather in Tokyo?\"" << std::endl; - std::cout << std::endl; - - std::string provider = "AnthropicProvider"; - std::string model = "claude-3-haiku-20240307"; - - // Connect to the GatewayServer which aggregates tools from multiple backends - std::string serverJson = R"({ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "servers": [ - { - "version": "2025-01-09", - "serverId": "1877234567890123456", - "name": "gopher-gateway-server", - "transport": "http_sse", - "config": { - "url": "http://127.0.0.1:3003/mcp", - "headers": {} - }, - "connectTimeout": 5000, - "requestTimeout": 30000 - } - ] - } - })"; - - // Parse queries from command line arguments - std::vector queries; - if (argc > 1) { - for (int i = 1; i < argc; i++) { - queries.push_back(argv[i]); - } - } else { - // Default queries using tools available through the gateway - queries.push_back("What is the weather in Tokyo?"); - } - - std::cout << "Provider: " << provider << std::endl; - std::cout << "Model: " << model << std::endl; - std::cout << "Gateway URL: http://127.0.0.1:3003/mcp" << std::endl; - std::cout << "Number of queries: " << queries.size() << std::endl; - std::cout << std::endl; - - std::cout << "Creating agent connected to gateway..." << std::endl; - - auto agent = ReActAgent::createByJson(provider, model, serverJson); - if (!agent) { - std::cout << "Error: Failed to create agent" << std::endl; - std::cout << "Make sure gateway_server_example is running on port 3003" << std::endl; - return 1; - } - - std::cout << "Agent created successfully!" << std::endl; - - // Execute all queries - for (size_t i = 0; i < queries.size(); i++) { - std::cout << "\n========================================" << std::endl; - std::cout << "Query " << (i + 1) << ": " << queries[i] << std::endl; - std::cout << "========================================" << std::endl; - - std::string answer = agent->run(queries[i]); - - std::cout << "\nAgent Response:" << std::endl; - std::cout << "----------------------------------------" << std::endl; - std::cout << answer << std::endl; - std::cout << "----------------------------------------" << std::endl; - } - - std::cout << "\n=== Test Complete ===" << std::endl; - return 0; -} diff --git a/third_party/gopher-orch/examples/sdk/test_error_response.cpp b/third_party/gopher-orch/examples/sdk/test_error_response.cpp deleted file mode 100644 index 5eae6f82..00000000 --- a/third_party/gopher-orch/examples/sdk/test_error_response.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @file test_error_response.cpp - * @brief Test error handling for API response validation - */ - -#include -#include - -#include "gopher/orch/agent/agent.h" - -using namespace gopher::orch::agent; - -int main() { - std::cout << "=== API Error Response Test ===" << std::endl; - - std::string provider = "AnthropicProvider"; - std::string model = "claude-3-haiku-20240307"; - - // Test case 1: API failure with custom error message - std::string failedApiJson = R"({ - "succeeded": false, - "code": 400000001, - "message": "Invalid server configuration provided", - "data": null - })"; - - std::cout << "\nTest 1: API failure response" << std::endl; - auto agent1 = ReActAgent::createByJson(provider, model, failedApiJson); - if (!agent1) { - std::cout << "✅ Agent creation correctly failed for API error response" << std::endl; - } else { - std::string result = agent1->run("test query"); - std::cout << "Expected error: " << result << std::endl; - } - - // Test case 2: API success but non-"success" message - std::string warningApiJson = R"({ - "succeeded": true, - "code": 200000001, - "message": "Server temporarily unavailable", - "data": { - "servers": [] - } - })"; - - std::cout << "\nTest 2: API success with warning message" << std::endl; - auto agent2 = ReActAgent::createByJson(provider, model, warningApiJson); - if (!agent2) { - std::cout << "✅ Agent creation correctly failed for non-success message" << std::endl; - } else { - std::string result = agent2->run("test query"); - std::cout << "Expected error: " << result << std::endl; - } - - // Test case 3: Invalid JSON - std::string invalidJson = R"({ - "succeeded": true, - "code": 200000000, - "message": "success", - "data": { - "servers": [ - invalid json here - ] - } - })"; - - std::cout << "\nTest 3: Invalid JSON format" << std::endl; - auto agent3 = ReActAgent::createByJson(provider, model, invalidJson); - if (!agent3) { - std::cout << "✅ Agent creation correctly failed for invalid JSON" << std::endl; - } else { - std::string result = agent3->run("test query"); - std::cout << "Expected error: " << result << std::endl; - } - - // Test case 4: Valid legacy format (should work) - std::string legacyJson = R"({ - "name": "test-config", - "mcp_servers": [] - })"; - - std::cout << "\nTest 4: Legacy format (should work)" << std::endl; - auto agent4 = ReActAgent::createByJson(provider, model, legacyJson); - if (agent4) { - std::cout << "✅ Agent creation succeeded for legacy format" << std::endl; - std::string result = agent4->run("test query"); - std::cout << "Result: " << result << std::endl; - } else { - std::cout << "❌ Agent creation unexpectedly failed for legacy format" << std::endl; - } - - return 0; -} \ No newline at end of file diff --git a/third_party/gopher-orch/examples/sdk/typescript/README.md b/third_party/gopher-orch/examples/sdk/typescript/README.md deleted file mode 100644 index d61e8ed3..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# TypeScript Examples for gopher-orch SDK - -This directory contains TypeScript examples that demonstrate how to use the gopher-orch library via FFI (Foreign Function Interface) bindings. - -## Features - -- **Real C++ FFI Integration**: Direct calls to the gopher-orch C++ library -- **Multiple Query Support**: Run multiple queries in sequence -- **Real AI Responses**: Get actual responses from Claude via MCP servers -- **Tool Discovery**: Automatically discovers MCP tools (time, weather, passwords, etc.) -- **Error Handling**: Comprehensive error handling with helpful messages - -## Available Examples - -### 1. JSON Config Example (`client_example_json.ts`) - -Uses a hardcoded JSON configuration with local MCP servers. - -```bash -# Run with default queries -./client_example_json_run.sh - -# Run with custom queries -./client_example_json_run.sh "What time is it in Tokyo?" "Generate a password" -``` - -### 2. API Config Example (`client_example_api.ts`) - -Fetches MCP server configuration from a remote API using an API key. - -```bash -# Run with default queries -./client_example_api_run.sh - -# Run with custom queries -./client_example_api_run.sh "What tools are available?" "What time is it?" -``` - -## Setup - -### Prerequisites - -1. **Build the C++ library**: - ```bash - cd /path/to/gopher-orch - make build - ``` - -2. **Set your API key** (for Anthropic): - ```bash - export ANTHROPIC_API_KEY="your-api-key-here" - ``` - -### Running Examples - -The run scripts handle everything automatically: -- Build the C++ library -- Copy shared libraries -- Build the TypeScript SDK -- Build and run the examples - -```bash -# JSON config example -./client_example_json_run.sh "What time is it in Tokyo?" - -# API config example -./client_example_api_run.sh "What tools are available?" -``` - -## Scripts - -```bash -npm run build # Compile TypeScript to JavaScript -npm run example:json # Run JSON config example -npm run example:api # Run API config example -npm run clean # Remove compiled files -``` - -## Available Tools - -The system automatically discovers these MCP tools: - -| Tool | Description | Example Query | -|------|-------------|---------------| -| `get-time` | Current time in any timezone | "What time is it in Tokyo?" | -| `get-weather` | Current weather for cities | "What's the weather in London?" | -| `generate-password` | Secure password generation | "Generate a 16-character password" | - -## Troubleshooting - -### Common Issues - -1. **"Agent error: invalid x-api-key"** - - Set a valid Anthropic API key: `export ANTHROPIC_API_KEY="sk-..."` - -2. **"No gopher-orch library found"** - - Run one of the build scripts: `./client_example_json_run.sh` - -3. **"Query execution timed out"** - - Check internet connection and try a simpler query - - MCP servers may be slow or unavailable - -4. **Process won't stop with Ctrl+C** - - The run scripts use `exec` to properly forward signals - - If still stuck, use `Ctrl+\` (SIGQUIT) or `kill` the process - -## License - -This project uses the same license as the main gopher-orch project. diff --git a/third_party/gopher-orch/examples/sdk/typescript/client_example_api_run.sh b/third_party/gopher-orch/examples/sdk/typescript/client_example_api_run.sh deleted file mode 100755 index d4405f5b..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/client_example_api_run.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# client_example_api_run.sh -# Build and run the TypeScript API example - -set -e - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="${SCRIPT_DIR}/../../.." -LOCAL_LIB_DIR="${SCRIPT_DIR}/lib" - -echo -e "${BLUE}=== Gopher-Orch TypeScript API Example ===${NC}\n" - -# Check prerequisites -for cmd in cmake node npm; do - if ! command -v "$cmd" &>/dev/null; then - echo -e "${RED}Error: $cmd not found${NC}" - exit 1 - fi -done - -# Build C++ library -echo -e "${YELLOW}>>> Building C++ library${NC}" -cd "${PROJECT_ROOT}" -mkdir -p build && cd build -[ ! -f "Makefile" ] && cmake .. -DCMAKE_BUILD_TYPE=Release -make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) -echo -e "${GREEN}✓ C++ library built${NC}\n" - -# Copy libraries -echo -e "${YELLOW}>>> Copying libraries${NC}" -mkdir -p "${LOCAL_LIB_DIR}" -cp "${PROJECT_ROOT}/build/lib/libgopher-orch."* "${LOCAL_LIB_DIR}/" 2>/dev/null || true -cp "${PROJECT_ROOT}/build/lib/libgopher-mcp."* "${LOCAL_LIB_DIR}/" 2>/dev/null || true -echo -e "${GREEN}✓ Libraries copied${NC}\n" - -# Build TypeScript SDK (always rebuild to use latest code) -echo -e "${YELLOW}>>> Building TypeScript SDK${NC}" -cd "${PROJECT_ROOT}/sdk/typescript" -npm install --silent -npm run build -echo -e "${GREEN}✓ SDK built${NC}\n" - -# Build examples (always rebuild to use latest code) -echo -e "${YELLOW}>>> Building examples${NC}" -cd "${SCRIPT_DIR}" -npm install --silent -npm run build -echo -e "${GREEN}✓ Examples built${NC}\n" - -# Set library path and run -if [[ "$OSTYPE" == "darwin"* ]]; then - export DYLD_LIBRARY_PATH="${LOCAL_LIB_DIR}:${DYLD_LIBRARY_PATH}" -else - export LD_LIBRARY_PATH="${LOCAL_LIB_DIR}:${LD_LIBRARY_PATH}" -fi - -echo -e "${BLUE}=== Running Example ===${NC}\n" -exec node dist/client_example_api.js diff --git a/third_party/gopher-orch/examples/sdk/typescript/client_example_json_run.sh b/third_party/gopher-orch/examples/sdk/typescript/client_example_json_run.sh deleted file mode 100755 index a395a511..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/client_example_json_run.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# client_example_json_run.sh -# Build and run the TypeScript JSON config example - -set -e - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="${SCRIPT_DIR}/../../.." -LOCAL_LIB_DIR="${SCRIPT_DIR}/lib" - -echo -e "${BLUE}=== Gopher-Orch TypeScript JSON Example ===${NC}\n" - -# Check prerequisites -for cmd in cmake node npm; do - if ! command -v "$cmd" &>/dev/null; then - echo -e "${RED}Error: $cmd not found${NC}" - exit 1 - fi -done - -# Build C++ library -echo -e "${YELLOW}>>> Building C++ library${NC}" -cd "${PROJECT_ROOT}" -mkdir -p build && cd build -[ ! -f "Makefile" ] && cmake .. -DCMAKE_BUILD_TYPE=Release -make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) -echo -e "${GREEN}✓ C++ library built${NC}\n" - -# Copy libraries -echo -e "${YELLOW}>>> Copying libraries${NC}" -mkdir -p "${LOCAL_LIB_DIR}" -cp "${PROJECT_ROOT}/build/lib/libgopher-orch."* "${LOCAL_LIB_DIR}/" 2>/dev/null || true -cp "${PROJECT_ROOT}/build/lib/libgopher-mcp."* "${LOCAL_LIB_DIR}/" 2>/dev/null || true -echo -e "${GREEN}✓ Libraries copied${NC}\n" - -# Build TypeScript SDK (always rebuild to use latest code) -echo -e "${YELLOW}>>> Building TypeScript SDK${NC}" -cd "${PROJECT_ROOT}/sdk/typescript" -npm install --silent -npm run build -echo -e "${GREEN}✓ SDK built${NC}\n" - -# Build examples (always rebuild to use latest code) -echo -e "${YELLOW}>>> Building examples${NC}" -cd "${SCRIPT_DIR}" -npm install --silent -npm run build -echo -e "${GREEN}✓ Examples built${NC}\n" - -# Set library path and run -if [[ "$OSTYPE" == "darwin"* ]]; then - export DYLD_LIBRARY_PATH="${LOCAL_LIB_DIR}:${DYLD_LIBRARY_PATH}" -else - export LD_LIBRARY_PATH="${LOCAL_LIB_DIR}:${LD_LIBRARY_PATH}" -fi - -echo -e "${BLUE}=== Running Example ===${NC}\n" -exec node dist/client_example_json.js diff --git a/third_party/gopher-orch/examples/sdk/typescript/package-lock.json b/third_party/gopher-orch/examples/sdk/typescript/package-lock.json deleted file mode 100644 index c1cc01e9..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/package-lock.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "gopher-orch-examples", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "gopher-orch-examples", - "version": "1.0.0", - "dependencies": { - "gopher-orch-sdk": "file:../../../sdk/typescript" - }, - "devDependencies": { - "@types/node": "^18.0.0", - "typescript": "^5.0.0" - } - }, - "../../../sdk/typescript": { - "name": "gopher-orch-sdk", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "koffi": "^2.8.0" - }, - "devDependencies": { - "@types/jest": "^29.0.0", - "@types/node": "^18.0.0", - "jest": "^29.0.0", - "typescript": "^5.0.0" - } - }, - "node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/gopher-orch-sdk": { - "resolved": "../../../sdk/typescript", - "link": true - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/third_party/gopher-orch/examples/sdk/typescript/package.json b/third_party/gopher-orch/examples/sdk/typescript/package.json deleted file mode 100644 index 849f3082..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "gopher-orch-examples", - "version": "1.0.0", - "description": "TypeScript examples for gopher-orch SDK", - "type": "module", - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "example:json": "node dist/client_example_json.js", - "example:api": "node dist/client_example_api.js", - "clean": "rm -rf dist" - }, - "dependencies": { - "gopher-orch-sdk": "file:../../../sdk/typescript" - }, - "devDependencies": { - "@types/node": "^18.0.0", - "typescript": "^5.0.0" - } -} diff --git a/third_party/gopher-orch/examples/sdk/typescript/src/client_example_api.ts b/third_party/gopher-orch/examples/sdk/typescript/src/client_example_api.ts deleted file mode 100644 index b836bed3..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/src/client_example_api.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @file client_example_api.ts - * @brief TypeScript example using remote API for server configuration - * - * Demonstrates the clean GopherAgent API for creating agents - * that fetch server configurations from a remote API. - */ -import { GopherAgent } from 'gopher-orch-sdk'; - -async function main(): Promise { - // Create agent with API key (fetches server config from remote API) - // Note: ANTHROPIC_API_KEY is read from environment by C++ layer - const provider = 'AnthropicProvider'; - const model = 'claude-3-haiku-20240307'; - const apiKey = 'sk_xkmdfiw3jfndeaypegwb'; - const agent = GopherAgent.create({ provider, model, apiKey }); - console.log('GopherAgent created!'); - - const question = 'What time is it in London?'; - console.log(`Question: ${question}`); - const answer = agent.run(question); - console.log('Answer:'); - console.log(answer); - - // Cleanup (optional - happens automatically on exit) - agent.dispose(); -} - -// Run -main().catch(error => { - console.error('Error:', error.message); - process.exit(1); -}); diff --git a/third_party/gopher-orch/examples/sdk/typescript/src/client_example_json.ts b/third_party/gopher-orch/examples/sdk/typescript/src/client_example_json.ts deleted file mode 100644 index 7571e417..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/src/client_example_json.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @file client_example_json.ts - * @brief TypeScript example using JSON server configuration - * - * Demonstrates the clean GopherAgent API for creating agents - * with hardcoded JSON server configurations. - */ - -import { GopherAgent } from 'gopher-orch-sdk'; - -// Server configuration JSON -const serverConfig = JSON.stringify({ - succeeded: true, - code: 200000000, - message: "success", - data: { - servers: [ - { - version: "2025-01-09", - serverId: "1877234567890123456", - name: "gopher-auth-server", - transport: "http_sse", - config: { - url: "http://127.0.0.1:3001/rpc", - headers: {} - }, - connectTimeout: 5000, - requestTimeout: 30000 - }, - { - version: "2025-01-09", - serverId: "1877234567890123457", - name: "gopher-auth-server2", - transport: "http_sse", - config: { - url: "http://127.0.0.1:3002/rpc", - headers: {} - }, - connectTimeout: 5000, - requestTimeout: 30000 - } - ] - } -}); - -async function main(): Promise { - console.log('=== Simple Agent Example ==='); - console.log('Usage: node client_example_json.js [query1] [query2] ...'); - console.log('Default queries if none provided:'); - console.log(' 1. What time is it in Tokyo?'); - console.log(' 2. Generate a 12-character password\n'); - - // Initialize the library - GopherAgent.init(); - - // Parse queries from command line arguments - const args = process.argv.slice(2); - const queries = args.length > 0 ? args : [ - 'What time is it in Tokyo?', - 'Generate a 12-character password' - ]; - - const provider = 'AnthropicProvider'; - const model = 'claude-3-haiku-20240307'; - - console.log(`Provider: ${provider}`); - console.log(`Model: ${model}`); - console.log(`Number of queries: ${queries.length}`); - console.log('Creating agent...'); - - try { - // Create agent with JSON server configuration - const agent = GopherAgent.create({ - provider, - model, - serverConfig - }); - - console.log('Agent created successfully!\n'); - - // Execute all queries - for (let i = 0; i < queries.length; i++) { - console.log(`Query ${i + 1}: ${queries[i]}`); - - const answer = agent.run(queries[i]); - - console.log(`\nAgent Response ${i + 1}:`); - console.log('--------------------------------'); - console.log(answer); - console.log('--------------------------------\n'); - } - - // Cleanup (optional - happens automatically on exit) - agent.dispose(); - - } catch (error: any) { - console.error('Error:', error.message); - process.exit(1); - } -} - -// Run -main().catch(error => { - console.error('Fatal error:', error); - process.exit(1); -}); diff --git a/third_party/gopher-orch/examples/sdk/typescript/tsconfig.json b/third_party/gopher-orch/examples/sdk/typescript/tsconfig.json deleted file mode 100644 index 50f44ac8..00000000 --- a/third_party/gopher-orch/examples/sdk/typescript/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/third_party/gopher-orch/examples/simple_agent/README.md b/third_party/gopher-orch/examples/simple_agent/README.md deleted file mode 100644 index eef8cd0c..00000000 --- a/third_party/gopher-orch/examples/simple_agent/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Simple ReAct Agent Example - -A basic AI agent that uses tools to answer questions using the ReAct (Reasoning + Acting) pattern. - -## What This Example Shows - -- Creating an LLM provider (OpenAI) -- Registering tools (calculator, weather, search) -- Building an AgentRunnable -- Observing agent steps with callbacks -- Running the agent to completion - -## Running - -```bash -# Build -cd build -make simple_agent - -# Run (requires OpenAI API key) -OPENAI_API_KEY=sk-... ./bin/simple_agent - -# Custom query -OPENAI_API_KEY=sk-... ./bin/simple_agent "What's 100/4?" -``` - -## Expected Output - -``` -Query: What's 10*5 and what's the weather in Tokyo? ----------------------------------------- - -[Step 1] Calling tools: calculator get_weather - -[Step 2] Response ready - -======================================== -Final Response: -The result of 10*5 is 50, and the weather in Tokyo is sunny with a -temperature of 72°F and 45% humidity. ----------------------------------------- -Iterations: 2 -Total tokens: 256 -``` - -## Code Walkthrough - -### 1. Create Provider -```cpp -auto provider = makeOpenAIProvider(api_key, "gpt-4"); -``` - -### 2. Register Tools -```cpp -auto registry = makeToolRegistry(); -registry->addSyncTool("calculator", ...); -registry->addTool("get_weather", ...); // async -``` - -### 3. Create Agent -```cpp -auto agent = makeAgentRunnable(provider, registry, config); -``` - -### 4. Run -```cpp -agent->invoke(query, config, dispatcher, callback); -``` - -## See Also - -- [Agent Framework](../../docs/Agent.md) -- [Tool Registry](../../docs/ToolRegistry.md) diff --git a/third_party/gopher-orch/examples/simple_agent/main.cc b/third_party/gopher-orch/examples/simple_agent/main.cc deleted file mode 100644 index 5fbbc453..00000000 --- a/third_party/gopher-orch/examples/simple_agent/main.cc +++ /dev/null @@ -1,165 +0,0 @@ -// Simple ReAct Agent Example -// -// Demonstrates a basic AI agent that uses tools to answer questions. -// The agent reasons about which tools to use and iterates until done. - -#include - -#include "gopher/orch/orch.h" - -using namespace gopher::orch; -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -int main(int argc, char* argv[]) { - // Check for API key - const char* api_key = std::getenv("OPENAI_API_KEY"); - if (!api_key) { - std::cerr << "Error: OPENAI_API_KEY environment variable not set\n"; - std::cerr << "Usage: OPENAI_API_KEY=sk-... ./simple_agent\n"; - return 1; - } - - // Create event dispatcher - auto dispatcher = mcp::event::createLibeventDispatcher(); - - // ========================================================================= - // Step 1: Create LLM Provider - // ========================================================================= - auto provider = makeOpenAIProvider(api_key, "gpt-4"); - - // ========================================================================= - // Step 2: Create Tool Registry with tools - // ========================================================================= - auto registry = makeToolRegistry(); - - // Calculator tool - synchronous - registry->addSyncTool( - "calculator", - "Perform mathematical calculations. Input: {\"expression\": \"2+2\"}", - JsonValue::object({{"expression", "string"}}), - [](const JsonValue& args) -> Result { - auto expr = args["expression"].getString(); - - // Simple expression evaluator (demo only) - double result = 0; - if (expr == "2+2") - result = 4; - else if (expr == "10*5") - result = 50; - else if (expr == "100/4") - result = 25; - else { - return makeOrchError(OrchError::INVALID_ARGUMENT, - "Cannot evaluate: " + expr); - } - - JsonValue response = JsonValue::object(); - response["result"] = result; - return makeSuccess(std::move(response)); - }); - - // Weather tool - async (simulated) - registry->addTool( - "get_weather", - "Get current weather for a city. Input: {\"city\": \"Tokyo\"}", - JsonValue::object({{"city", "string"}}), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - auto city = args["city"].getString(); - - // Simulate async API call - d.post([city, cb = std::move(cb)]() { - JsonValue weather = JsonValue::object(); - weather["city"] = city; - weather["temperature"] = 72; - weather["condition"] = "sunny"; - weather["humidity"] = 45; - cb(makeSuccess(std::move(weather))); - }); - }); - - // Search tool - async (simulated) - registry->addTool( - "search", "Search the web for information. Input: {\"query\": \"...\"}", - JsonValue::object({{"query", "string"}}), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - auto query = args["query"].getString(); - - d.post([query, cb = std::move(cb)]() { - JsonValue results = JsonValue::object(); - results["query"] = query; - results["results"] = JsonValue::array({ - JsonValue("Result 1: " + query + " - relevant information..."), - JsonValue("Result 2: More details about " + query), - }); - cb(makeSuccess(std::move(results))); - }); - }); - - // ========================================================================= - // Step 3: Create Agent - // ========================================================================= - auto agent = makeAgentRunnable( - provider, registry, - AgentConfig("gpt-4") - .withSystemPrompt( - "You are a helpful assistant with access to tools. " - "Use the calculator for math, get_weather for weather info, " - "and search for general questions. " - "Always explain your reasoning.") - .withMaxIterations(5)); - - // Optional: Set step callback for observability - agent->setStepCallback([](const AgentStep& step) { - std::cout << "\n[Step " << step.step_number << "] "; - if (step.llm_message.hasToolCalls()) { - std::cout << "Calling tools: "; - for (const auto& call : *step.llm_message.tool_calls) { - std::cout << call.name << " "; - } - } else { - std::cout << "Response ready"; - } - std::cout << std::endl; - }); - - // ========================================================================= - // Step 4: Run Agent with a query - // ========================================================================= - std::string query = "What's 10*5 and what's the weather in Tokyo?"; - if (argc > 1) { - query = argv[1]; - } - - std::cout << "Query: " << query << "\n"; - std::cout << "----------------------------------------\n"; - - bool done = false; - agent->invoke( - JsonValue(query), RunnableConfig(), *dispatcher, - [&done](Result result) { - if (mcp::holds_alternative(result)) { - std::cerr << "Error: " << mcp::get(result).message << "\n"; - } else { - auto& output = mcp::get(result); - std::cout << "\n========================================\n"; - std::cout << "Final Response:\n"; - std::cout << output["response"].getString() << "\n"; - std::cout << "----------------------------------------\n"; - std::cout << "Iterations: " << output["iterations"].getInt() << "\n"; - if (output.contains("usage")) { - std::cout << "Total tokens: " - << output["usage"]["total_tokens"].getInt() << "\n"; - } - } - done = true; - }); - - // Run event loop until done - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - - return 0; -} diff --git a/third_party/gopher-orch/examples/workflow/README.md b/third_party/gopher-orch/examples/workflow/README.md deleted file mode 100644 index b59c7e17..00000000 --- a/third_party/gopher-orch/examples/workflow/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# StateGraph Workflow Example - -A document processing workflow demonstrating StateGraph with conditional branching. - -## What This Example Shows - -- Building a StateGraph with multiple nodes -- State merging with reducer functions -- Conditional edge routing -- Processing multiple documents through the workflow -- LangGraph-style graph compilation - -## Running - -```bash -# Build -cd build -make workflow - -# Run -./bin/workflow -``` - -## Expected Output - -``` -======================================== -Document 1: -"This API function returns a JSON response with the user data." ----------------------------------------- -Classification: technical -Word count: 11 -Summary: Technical document summary: This API function returns a JSON response... -Keywords: technical, documentation, API - -======================================== -Document 2: -"This agreement constitutes the entire contract between parties." ----------------------------------------- -Classification: legal -Word count: 8 -Summary: Legal document summary: This agreement constitutes the entire contract... -Keywords: legal, contract, agreement -*** Flagged for review *** - -======================================== -Document 3: -"The weather today is sunny with a high of 75 degrees." ----------------------------------------- -Classification: general -Word count: 11 -Summary: General document summary: The weather today is sunny with a high of 75... -Keywords: general, document - -======================================== -All documents processed. -``` - -## Workflow Structure - -``` -START -> count_words -> classify -> [conditional branch] - | - +-----------------+------------------+ - | | | - technical legal general - | | | - summarize_tech summarize_legal summarize_general - | | | - +-----------------+------------------+ - | - finalize -> END -``` - -## Code Walkthrough - -### 1. Define State Structure -```cpp -struct DocumentState { - std::string content; - std::string classification; - std::string summary; - std::vector keywords; - bool needs_review = false; - int word_count = 0; - - static DocumentState merge(const DocumentState& base, - const DocumentState& update); -}; -``` - -### 2. Define Node Functions -```cpp -DocumentState classifyDocument(const DocumentState& state, Dispatcher& d) { - DocumentState update; - // Classification logic... - update.classification = "technical"; - return update; -} -``` - -### 3. Define Router Function -```cpp -std::string routeByClassification(const DocumentState& state) { - if (state.classification == "technical") { - return "summarize_technical"; - } else if (state.classification == "legal") { - return "summarize_legal"; - } - return "summarize_general"; -} -``` - -### 4. Build Graph -```cpp -auto graph = StateGraphBuilder() - .addNode("classify", classifyDocument) - .addNode("summarize_technical", summarizeTechnical) - // ...more nodes... - .addEdge(START, "classify") - .addConditionalEdge("classify", routeByClassification, { - {"summarize_technical", "summarize_technical"}, - {"summarize_legal", "summarize_legal"}, - {"summarize_general", "summarize_general"} - }) - .compile(); -``` - -### 5. Execute Workflow -```cpp -DocumentState initial; -initial.content = "Document content..."; - -graph->invoke(initial, config, dispatcher, [](Result result) { - const auto& state = mcp::get(result); - std::cout << "Classification: " << state.classification << "\n"; -}); -``` - -## Key Concepts - -- **State**: Immutable data structure passed between nodes -- **Nodes**: Functions that transform state -- **Edges**: Define execution flow between nodes -- **Conditional Edges**: Route based on state values -- **Reducer**: Merges partial state updates - -## See Also - -- [StateGraph Guide](../../docs/StateGraph.md) -- [Runnable Interface](../../docs/Runnable.md) diff --git a/third_party/gopher-orch/examples/workflow/main.cc b/third_party/gopher-orch/examples/workflow/main.cc deleted file mode 100644 index 2531f119..00000000 --- a/third_party/gopher-orch/examples/workflow/main.cc +++ /dev/null @@ -1,230 +0,0 @@ -// StateGraph Workflow Example -// -// Demonstrates a document processing workflow using StateGraph. -// Shows conditional branching, node execution, and state management. - -#include -#include - -#include "gopher/orch/orch.h" - -using namespace gopher::orch; -using namespace gopher::orch::graph; -using namespace gopher::orch::core; - -// Document processing state -struct DocumentState { - std::string content; - std::string classification; // "technical", "legal", "general" - std::string summary; - std::vector keywords; - bool needs_review = false; - int word_count = 0; - - // Merge function for state updates - static DocumentState merge(const DocumentState& base, - const DocumentState& update) { - DocumentState result = base; - if (!update.content.empty()) - result.content = update.content; - if (!update.classification.empty()) - result.classification = update.classification; - if (!update.summary.empty()) - result.summary = update.summary; - if (!update.keywords.empty()) - result.keywords = update.keywords; - if (update.needs_review) - result.needs_review = update.needs_review; - if (update.word_count > 0) - result.word_count = update.word_count; - return result; - } -}; - -// Count words in document -DocumentState countWords(const DocumentState& state, Dispatcher& d) { - DocumentState update; - int count = 0; - bool in_word = false; - for (char c : state.content) { - if (std::isspace(c)) { - in_word = false; - } else if (!in_word) { - in_word = true; - count++; - } - } - update.word_count = count; - return update; -} - -// Classify document based on content -DocumentState classifyDocument(const DocumentState& state, Dispatcher& d) { - DocumentState update; - - // Simple keyword-based classification - const std::string& content = state.content; - if (content.find("API") != std::string::npos || - content.find("function") != std::string::npos || - content.find("code") != std::string::npos) { - update.classification = "technical"; - } else if (content.find("agreement") != std::string::npos || - content.find("contract") != std::string::npos || - content.find("liability") != std::string::npos) { - update.classification = "legal"; - update.needs_review = true; // Legal docs need review - } else { - update.classification = "general"; - } - - return update; -} - -// Generate summary for technical documents -DocumentState summarizeTechnical(const DocumentState& state, Dispatcher& d) { - DocumentState update; - update.summary = - "Technical document summary: " + - state.content.substr(0, std::min(size_t(50), state.content.size())) + - "..."; - update.keywords = {"technical", "documentation", "API"}; - return update; -} - -// Generate summary for legal documents -DocumentState summarizeLegal(const DocumentState& state, Dispatcher& d) { - DocumentState update; - update.summary = - "Legal document summary: " + - state.content.substr(0, std::min(size_t(50), state.content.size())) + - "..."; - update.keywords = {"legal", "contract", "agreement"}; - return update; -} - -// Generate summary for general documents -DocumentState summarizeGeneral(const DocumentState& state, Dispatcher& d) { - DocumentState update; - update.summary = - "General document summary: " + - state.content.substr(0, std::min(size_t(50), state.content.size())) + - "..."; - update.keywords = {"general", "document"}; - return update; -} - -// Finalize processing -DocumentState finalize(const DocumentState& state, Dispatcher& d) { - // No state changes, just a pass-through node - return DocumentState(); -} - -// Router function for conditional branching -std::string routeByClassification(const DocumentState& state) { - if (state.classification == "technical") { - return "summarize_technical"; - } else if (state.classification == "legal") { - return "summarize_legal"; - } else { - return "summarize_general"; - } -} - -int main() { - auto dispatcher = mcp::event::createLibeventDispatcher(); - - // ========================================================================= - // Build StateGraph for document processing - // ========================================================================= - // - // Workflow structure: - // START -> count_words -> classify -> [conditional branch] - // | - // +-----------------+------------------+ - // | | | - // technical legal general - // | | | - // summarize_tech summarize_legal summarize_general - // | | | - // +-----------------+------------------+ - // | - // finalize -> END - - auto graph = - StateGraphBuilder() - .addNode("count_words", countWords) - .addNode("classify", classifyDocument) - .addNode("summarize_technical", summarizeTechnical) - .addNode("summarize_legal", summarizeLegal) - .addNode("summarize_general", summarizeGeneral) - .addNode("finalize", finalize) - // Define edges - .addEdge(START, "count_words") - .addEdge("count_words", "classify") - // Conditional routing based on classification - .addConditionalEdge("classify", routeByClassification, - {{"summarize_technical", "summarize_technical"}, - {"summarize_legal", "summarize_legal"}, - {"summarize_general", "summarize_general"}}) - // All summarization nodes lead to finalize - .addEdge("summarize_technical", "finalize") - .addEdge("summarize_legal", "finalize") - .addEdge("summarize_general", "finalize") - .addEdge("finalize", END) - .compile(); - - // ========================================================================= - // Process sample documents - // ========================================================================= - - std::vector documents = { - "This API function returns a JSON response with the user data.", - "This agreement constitutes the entire contract between parties.", - "The weather today is sunny with a high of 75 degrees.", - }; - - for (size_t i = 0; i < documents.size(); i++) { - std::cout << "\n========================================\n"; - std::cout << "Document " << (i + 1) << ":\n"; - std::cout << "\"" << documents[i] << "\"\n"; - std::cout << "----------------------------------------\n"; - - // Create initial state - DocumentState initial; - initial.content = documents[i]; - - bool done = false; - graph->invoke( - initial, RunnableConfig(), *dispatcher, - [&done](Result result) { - if (mcp::holds_alternative(result)) { - std::cerr << "Error: " << mcp::get(result).message << "\n"; - } else { - const auto& state = mcp::get(result); - std::cout << "Classification: " << state.classification << "\n"; - std::cout << "Word count: " << state.word_count << "\n"; - std::cout << "Summary: " << state.summary << "\n"; - std::cout << "Keywords: "; - for (size_t j = 0; j < state.keywords.size(); j++) { - if (j > 0) - std::cout << ", "; - std::cout << state.keywords[j]; - } - std::cout << "\n"; - if (state.needs_review) { - std::cout << "*** Flagged for review ***\n"; - } - } - done = true; - }); - - while (!done) { - dispatcher->run(mcp::event::Dispatcher::RunType::NonBlock); - } - } - - std::cout << "\n========================================\n"; - std::cout << "All documents processed.\n"; - - return 0; -} diff --git a/third_party/gopher-orch/include/gopher/orch/agent/agent.h b/third_party/gopher-orch/include/gopher/orch/agent/agent.h deleted file mode 100644 index 3794840a..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/agent.h +++ /dev/null @@ -1,205 +0,0 @@ -#pragma once - -// Agent - ReAct-style AI agent implementation -// -// Implements the ReAct (Reasoning + Acting) pattern: -// 1. LLM receives user query and available tools -// 2. LLM reasons and decides to either respond or use tools -// 3. If tool calls requested, execute them -// 4. Feed tool results back to LLM -// 5. Repeat until LLM provides final response -// -// Usage: -// auto provider = createOpenAIProvider("sk-..."); -// auto registry = makeToolRegistry(); -// registry->addTool("search", "Search the web", schema, searchFunc); -// -// AgentConfig config("gpt-4o"); -// config.withSystemPrompt("You are a helpful assistant."); -// -// auto agent = ReActAgent::create(provider, registry, config); -// agent->run("What's the weather in Tokyo?", dispatcher, callback); - -#include -#include -#include -#include - -#include "gopher/orch/agent/agent_types.h" -#include "gopher/orch/agent/tool_executor.h" -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/llm/llm_provider.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::llm; - -// Forward declarations -class Agent; -class ToolsFetcher; -using AgentPtr = std::shared_ptr; - -// Agent - Abstract base class for AI agents -class Agent { - public: - virtual ~Agent() = default; - - // Run the agent with a user query - virtual void run(const std::string& query, - Dispatcher& dispatcher, - AgentCallback callback) = 0; - - // Run with additional context messages - virtual void run(const std::string& query, - const std::vector& context, - Dispatcher& dispatcher, - AgentCallback callback) = 0; - - // Cancel a running agent - virtual void cancel() = 0; - - // Get current state - virtual const AgentState& state() const = 0; - - // Check if running - virtual bool isRunning() const = 0; - - // Set step callback for progress monitoring - virtual void setStepCallback(StepCallback callback) = 0; - - // Set tool approval callback - virtual void setToolApprovalCallback(ToolApprovalCallback callback) = 0; -}; - -// ReActAgent - Implementation of ReAct pattern -// -// Thread Safety: -// - run() should be called from dispatcher thread -// - cancel() can be called from any thread -// - Callbacks are invoked in dispatcher thread context -class ReActAgent : public Agent { - public: - using Ptr = std::shared_ptr; - - // Factory methods - static Ptr create(LLMProviderPtr provider, - ToolRegistryPtr tools, - const AgentConfig& config = AgentConfig()); - - static Ptr create(LLMProviderPtr provider, - const AgentConfig& config = AgentConfig()); - - ~ReActAgent() override; - - // Agent interface - void run(const std::string& query, - Dispatcher& dispatcher, - AgentCallback callback) override; - - void run(const std::string& query, - const std::vector& context, - Dispatcher& dispatcher, - AgentCallback callback) override; - - void cancel() override; - - const AgentState& state() const override; - bool isRunning() const override; - - void setStepCallback(StepCallback callback) override; - void setToolApprovalCallback(ToolApprovalCallback callback) override; - - // ReActAgent-specific methods - - // Get the LLM provider - LLMProviderPtr provider() const; - - // Get the tool registry - ToolRegistryPtr tools() const; - - // Get configuration - const AgentConfig& config() const; - - // Update configuration (only when not running) - void setConfig(const AgentConfig& config); - - // Add tools dynamically - void addTool(const std::string& name, - const std::string& description, - const JsonValue& parameters, - ToolFunction function); - - // Simple agent creation and execution - - // Create agent from provider name, model, and server config JSON - // Returns configured agent ready for multiple queries - static Ptr createByJson(const std::string& provider_name, - const std::string& model, - const std::string& server_json_config); - - // Create agent from provider name, model, and API key - // Fetches server config from remote API using the provided API key - static Ptr createByApiKey(const std::string& provider_name, - const std::string& model, - const std::string& api_key); - - // Simple synchronous run method for queries - // Returns the final response string from the agent - std::string run(const std::string& query); - - private: - explicit ReActAgent(LLMProviderPtr provider, - ToolRegistryPtr tools, - const AgentConfig& config); - - // For simple agent creation (createByJson/createByApiKey) - std::string server_json_config_; - bool tools_loaded_ = true; // Default to true for normal creation - - // Keep MCP server connections alive for the lifetime of the agent - // These are only used when agent is created via createByJson/createByApiKey - std::unique_ptr tools_fetcher_; - std::unique_ptr owned_dispatcher_; - - // Helper methods for simple agent usage - bool loadTools(); - std::string runWithLoadedAgent(const std::string& query); - - // Shutdown MCP connections (called by destructor) - void shutdownConnections(); - - // Internal execution methods - void executeLoop(Dispatcher& dispatcher); - void callLLM(Dispatcher& dispatcher); - void handleLLMResponse(const LLMResponse& response, Dispatcher& dispatcher); - void executeToolCalls(const std::vector& calls, - Dispatcher& dispatcher); - void handleToolResults(const std::vector& calls, - const std::vector>& results, - Dispatcher& dispatcher); - void completeRun(AgentStatus status, Dispatcher& dispatcher); - - // Build result from current state - AgentResult buildResult() const; - - class Impl; - std::unique_ptr impl_; -}; - -// Convenience function to create agent -inline AgentPtr makeAgent(LLMProviderPtr provider, - ToolRegistryPtr tools, - const AgentConfig& config = AgentConfig()) { - return ReActAgent::create(provider, tools, config); -} - -inline AgentPtr makeAgent(LLMProviderPtr provider, - const AgentConfig& config = AgentConfig()) { - return ReActAgent::create(provider, config); -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/agent_module.h b/third_party/gopher-orch/include/gopher/orch/agent/agent_module.h deleted file mode 100644 index 001c37cc..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/agent_module.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -// Agent Module - AI agent framework with ReAct pattern -// -// This module provides: -// - Agent: Abstract interface for AI agents -// - ReActAgent: ReAct pattern implementation (Reasoning + Acting) -// - ToolRegistry: Unified tool management from multiple sources -// - AgentConfig, AgentState, AgentResult: Configuration and state types -// -// Usage: -// #include "gopher/orch/agent/agent_module.h" -// using namespace gopher::orch::agent; -// -// // Create LLM provider -// auto provider = createOpenAIProvider("sk-..."); -// -// // Create tool registry and add tools -// auto registry = makeToolRegistry(); -// registry->addTool("search", "Search the web", schema, -// [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { -// // Search implementation... -// }); -// -// // Create and configure agent -// AgentConfig config("gpt-4o"); -// config.withSystemPrompt("You are a helpful research assistant.") -// .withMaxIterations(10); -// -// auto agent = makeAgent(provider, registry, config); -// -// // Run agent -// agent->run("What's the latest news about AI?", dispatcher, -// [](Result result) { -// if (result.isOk()) { -// std::cout << result.value().response << std::endl; -// } -// }); - -// Core types -#include "gopher/orch/agent/agent_types.h" - -// API configuration -#include "gopher/orch/agent/api_engine.h" - -// Tool definitions and configuration -#include "gopher/orch/agent/config_loader.h" -#include "gopher/orch/agent/rest_tool_adapter.h" -#include "gopher/orch/agent/tool_definition.h" - -// Tool management -#include "gopher/orch/agent/tool_registry.h" - -// Agent interface and implementations -#include "gopher/orch/agent/agent.h" - -namespace gopher { -namespace orch { -namespace agent { - -// Convenience re-exports -using core::Dispatcher; -using core::Error; -using core::JsonCallback; -using core::JsonValue; -using core::Result; - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/agent_runnable.h b/third_party/gopher-orch/include/gopher/orch/agent/agent_runnable.h deleted file mode 100644 index fffa313b..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/agent_runnable.h +++ /dev/null @@ -1,216 +0,0 @@ -#pragma once - -// AgentRunnable - Wraps ReAct Agent as a composable Runnable -// -// Makes the ReAct agent pattern composable with other Runnables in pipelines, -// sequences, and graphs. Internally operates as a graph with LLM and Tool -// nodes. -// -// This is the main integration point for agent + runnable composition, -// implementing the wrapper pattern (Option A from design doc). -// -// Usage: -// auto provider = createOpenAIProvider("sk-..."); -// auto registry = makeToolRegistry(); -// registry->addTool("search", "Search", schema, handler); -// -// auto agent = AgentRunnable::create(provider, registry, -// AgentConfig("gpt-4").withSystemPrompt("You are helpful")); -// -// JsonValue input = JsonValue::object(); -// input["query"] = "What is the weather in Tokyo?"; -// -// agent->invoke(input, config, dispatcher, callback); - -#include -#include - -#include "gopher/orch/agent/agent_types.h" -#include "gopher/orch/agent/tool_executor.h" -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/core/runnable.h" -#include "gopher/orch/llm/llm_provider.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; -using namespace gopher::orch::llm; - -// Forward declaration -class AgentRunnable; -using AgentRunnablePtr = std::shared_ptr; - -// AgentRunnable - ReAct Agent as a Runnable -// -// Input Schema: -// { -// "query": "What is the weather?", // Required -// "context": [...], // Optional: prior messages -// "config": { // Optional: override config -// "max_iterations": 5, -// "system_prompt": "..." -// } -// } -// -// Alternative inputs (auto-detected): -// - Simple string: "What is the weather?" -// - LangGraph-style: {"messages": [...]} -// -// Output Schema: -// { -// "response": "The weather is sunny.", -// "status": "completed", -// "iterations": 2, -// "messages": [...], -// "usage": {...}, -// "duration_ms": 3500 -// } -class AgentRunnable : public Runnable { - public: - using Ptr = std::shared_ptr; - - // Factory methods - static Ptr create(LLMProviderPtr provider, - ToolExecutorPtr executor, - const AgentConfig& config = AgentConfig()); - - static Ptr create(LLMProviderPtr provider, - ToolRegistryPtr registry, - const AgentConfig& config = AgentConfig()); - - static Ptr create(LLMProviderPtr provider, - const AgentConfig& config = AgentConfig()); - - // Runnable interface - std::string name() const override; - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override; - - // ========================================================================= - // CONFIGURATION - // ========================================================================= - - // Get/set config - const AgentConfig& config() const { return config_; } - void setConfig(const AgentConfig& config) { config_ = config; } - - // Get components - LLMProviderPtr provider() const { return provider_; } - ToolExecutorPtr executor() const { return executor_; } - ToolRegistryPtr registry() const { - return executor_ ? executor_->registry() : nullptr; - } - - // ========================================================================= - // CALLBACKS - // ========================================================================= - - // Called after each step (LLM call + tool executions) - void setStepCallback(StepCallback callback) { - step_callback_ = std::move(callback); - } - - // Called before tool execution for approval - void setToolApprovalCallback(ToolApprovalCallback callback) { - approval_callback_ = std::move(callback); - } - - private: - AgentRunnable(LLMProviderPtr provider, - ToolExecutorPtr executor, - const AgentConfig& config); - - // ========================================================================= - // INPUT PARSING - // ========================================================================= - - struct ParsedInput { - std::string query; - std::vector context; - AgentConfig config; - }; - ParsedInput parseInput(const JsonValue& input) const; - - // ========================================================================= - // AGENT LOOP EXECUTION - // ========================================================================= - - // Execute the ReAct loop - void executeLoop(AgentState& state, - Dispatcher& dispatcher, - Callback callback); - - // Call LLM with current state - void callLLM(AgentState& state, Dispatcher& dispatcher, Callback callback); - - // Handle LLM response (may call tools or complete) - void handleLLMResponse(const LLMResponse& response, - AgentState& state, - Dispatcher& dispatcher, - Callback callback); - - // Execute tool calls - void executeTools(const std::vector& calls, - AgentState& state, - Dispatcher& dispatcher, - Callback callback); - - // Complete the agent run (success or failure) - void completeRun(AgentState& state, Callback callback); - - // ========================================================================= - // OUTPUT BUILDING - // ========================================================================= - - // Build output JSON from final state - JsonValue buildOutput(const AgentState& state) const; - - // ========================================================================= - // HELPERS - // ========================================================================= - - // Build messages array for LLM call - std::vector buildMessages(const AgentState& state) const; - - // Get tool specs for LLM - std::vector getToolSpecs() const; - - // Check if should continue loop - bool shouldContinue(const AgentState& state) const; - - // Record a step - void recordStep(AgentState& state, - const Message& llm_message, - const optional& usage, - std::chrono::milliseconds llm_duration); - - LLMProviderPtr provider_; - ToolExecutorPtr executor_; - AgentConfig config_; - - StepCallback step_callback_; - ToolApprovalCallback approval_callback_; -}; - -// Convenience factory functions -inline AgentRunnablePtr makeAgentRunnable( - LLMProviderPtr provider, - ToolRegistryPtr registry, - const AgentConfig& config = AgentConfig()) { - return AgentRunnable::create(std::move(provider), std::move(registry), - config); -} - -inline AgentRunnablePtr makeAgentRunnable( - LLMProviderPtr provider, const AgentConfig& config = AgentConfig()) { - return AgentRunnable::create(std::move(provider), config); -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/agent_types.h b/third_party/gopher-orch/include/gopher/orch/agent/agent_types.h deleted file mode 100644 index 9a63553e..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/agent_types.h +++ /dev/null @@ -1,484 +0,0 @@ -#pragma once - -// Agent Types - Core types for AI agent implementation -// -// Provides configuration, state, and result types for running -// ReAct-style agents that combine LLM reasoning with tool execution. - -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" -#include "gopher/orch/llm/llm_types.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; -using namespace gopher::orch::llm; - -// ═══════════════════════════════════════════════════════════════════════════ -// AGENT CONFIGURATION -// ═══════════════════════════════════════════════════════════════════════════ - -struct AgentConfig { - // LLM configuration - LLMConfig llm_config; - - // System prompt for the agent - std::string system_prompt; - - // Maximum iterations in the ReAct loop (prevents infinite loops) - int max_iterations = 10; - - // Maximum total tokens across all iterations - optional max_total_tokens; - - // Timeout for entire agent run - std::chrono::milliseconds timeout{300000}; // 5 minutes default - - // Tool execution settings - bool parallel_tool_calls = true; // Execute multiple tool calls in parallel - - // Callbacks - bool enable_step_callbacks = true; - - AgentConfig() = default; - - explicit AgentConfig(const std::string& model) : llm_config(model) {} - - AgentConfig& withModel(const std::string& model) { - llm_config.model = model; - return *this; - } - - AgentConfig& withSystemPrompt(const std::string& prompt) { - system_prompt = prompt; - return *this; - } - - AgentConfig& withTemperature(double t) { - llm_config.temperature = t; - return *this; - } - - AgentConfig& withMaxTokens(int tokens) { - llm_config.max_tokens = tokens; - return *this; - } - - AgentConfig& withMaxIterations(int iterations) { - max_iterations = iterations; - return *this; - } - - AgentConfig& withTimeout(std::chrono::milliseconds t) { - timeout = t; - return *this; - } - - AgentConfig& withParallelToolCalls(bool enabled) { - parallel_tool_calls = enabled; - return *this; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// AGENT STATE -// ═══════════════════════════════════════════════════════════════════════════ - -// Current state of agent execution -enum class AgentStatus { - IDLE, // Not started - RUNNING, // Currently executing - COMPLETED, // Finished successfully - FAILED, // Error occurred - CANCELLED, // Cancelled by user - MAX_ITERATIONS_REACHED // Hit iteration limit -}; - -// Convert status to string -inline std::string agentStatusToString(AgentStatus status) { - switch (status) { - case AgentStatus::IDLE: - return "idle"; - case AgentStatus::RUNNING: - return "running"; - case AgentStatus::COMPLETED: - return "completed"; - case AgentStatus::FAILED: - return "failed"; - case AgentStatus::CANCELLED: - return "cancelled"; - case AgentStatus::MAX_ITERATIONS_REACHED: - return "max_iterations_reached"; - default: - return "unknown"; - } -} - -// Record of a single tool execution -struct ToolExecution { - std::string tool_name; - std::string call_id; - JsonValue input; - JsonValue output; - bool success = true; - std::string error_message; - std::chrono::milliseconds duration{0}; -}; - -// Record of a single agent step (one LLM call + tool executions) -struct AgentStep { - int step_number = 0; - - // LLM response for this step - Message llm_message; - optional llm_usage; - - // Tool executions (if any) - std::vector tool_executions; - - // Timing - std::chrono::milliseconds llm_duration{0}; - std::chrono::milliseconds tools_duration{0}; -}; - -// Current state during agent execution -// -// Supports reducer-based state updates for graph-style execution. -// Messages use APPEND semantics (like LangGraph's add_messages), -// other fields use last-write-wins semantics. -struct AgentState { - AgentStatus status = AgentStatus::IDLE; - - // Conversation history (uses APPEND reducer) - std::vector messages; - - // Steps taken (uses APPEND reducer) - std::vector steps; - - // Current iteration (last-write-wins) - int current_iteration = 0; - - // Remaining steps before max iterations (last-write-wins) - int remaining_steps = 10; - - // Token usage (accumulated) - Usage total_usage; - - // Timing - std::chrono::steady_clock::time_point start_time; - std::chrono::milliseconds elapsed{0}; - - // Error info (if failed, last-write-wins) - optional error; - - // Check if agent is still running - bool isRunning() const { return status == AgentStatus::RUNNING; } - - // Check if agent completed successfully - bool isCompleted() const { return status == AgentStatus::COMPLETED; } - - // Get last message content - std::string lastContent() const { - if (messages.empty()) - return ""; - return messages.back().content; - } - - // ========================================================================= - // REDUCER - Merges state updates following LangGraph semantics - // ========================================================================= - - // Reduce (merge) two states. Used by graph execution to combine node outputs. - // - messages: APPEND (new messages are appended to existing) - // - steps: APPEND (new steps are appended) - // - current_iteration: last-write-wins - // - remaining_steps: last-write-wins - // - total_usage: accumulated (tokens are added) - // - status, error: last-write-wins - static AgentState reduce(const AgentState& current, - const AgentState& update) { - AgentState result; - - // APPEND: messages - result.messages = current.messages; - for (const auto& msg : update.messages) { - result.messages.push_back(msg); - } - - // APPEND: steps - result.steps = current.steps; - for (const auto& step : update.steps) { - result.steps.push_back(step); - } - - // LAST-WRITE-WINS: other fields - result.status = update.status; - result.current_iteration = update.current_iteration; - result.remaining_steps = update.remaining_steps; - result.error = update.error; - result.elapsed = update.elapsed; - result.start_time = update.start_time; - - // ACCUMULATE: token usage - result.total_usage.prompt_tokens = - current.total_usage.prompt_tokens + update.total_usage.prompt_tokens; - result.total_usage.completion_tokens = - current.total_usage.completion_tokens + - update.total_usage.completion_tokens; - result.total_usage.total_tokens = - current.total_usage.total_tokens + update.total_usage.total_tokens; - - return result; - } - - // ========================================================================= - // JSON SERIALIZATION - For graph node I/O - // ========================================================================= - - // Convert state to JSON for passing between graph nodes - JsonValue toJson() const { - JsonValue json = JsonValue::object(); - - json["status"] = agentStatusToString(status); - json["current_iteration"] = current_iteration; - json["remaining_steps"] = remaining_steps; - - // Messages array - JsonValue messages_arr = JsonValue::array(); - for (const auto& msg : messages) { - JsonValue msg_json = JsonValue::object(); - msg_json["role"] = roleToString(msg.role); - msg_json["content"] = msg.content; - if (msg.tool_call_id.has_value()) { - msg_json["tool_call_id"] = *msg.tool_call_id; - } - if (msg.hasToolCalls()) { - JsonValue calls_arr = JsonValue::array(); - for (const auto& call : *msg.tool_calls) { - JsonValue call_json = JsonValue::object(); - call_json["id"] = call.id; - call_json["name"] = call.name; - call_json["arguments"] = call.arguments; - calls_arr.push_back(call_json); - } - msg_json["tool_calls"] = calls_arr; - } - messages_arr.push_back(msg_json); - } - json["messages"] = messages_arr; - - // Usage - JsonValue usage_json = JsonValue::object(); - usage_json["prompt_tokens"] = total_usage.prompt_tokens; - usage_json["completion_tokens"] = total_usage.completion_tokens; - usage_json["total_tokens"] = total_usage.total_tokens; - json["usage"] = usage_json; - - // Error if present - if (error.has_value()) { - JsonValue err_json = JsonValue::object(); - err_json["code"] = error->code; - err_json["message"] = error->message; - json["error"] = err_json; - } - - return json; - } - - // Parse state from JSON - static AgentState fromJson(const JsonValue& json) { - AgentState state; - - if (!json.isObject()) { - return state; - } - - // Parse status - if (json.contains("status") && json["status"].isString()) { - std::string status_str = json["status"].getString(); - if (status_str == "idle") - state.status = AgentStatus::IDLE; - else if (status_str == "running") - state.status = AgentStatus::RUNNING; - else if (status_str == "completed") - state.status = AgentStatus::COMPLETED; - else if (status_str == "failed") - state.status = AgentStatus::FAILED; - else if (status_str == "cancelled") - state.status = AgentStatus::CANCELLED; - else if (status_str == "max_iterations_reached") - state.status = AgentStatus::MAX_ITERATIONS_REACHED; - } - - // Parse iteration counts - if (json.contains("current_iteration") && - json["current_iteration"].isNumber()) { - state.current_iteration = json["current_iteration"].getInt(); - } - if (json.contains("remaining_steps") && - json["remaining_steps"].isNumber()) { - state.remaining_steps = json["remaining_steps"].getInt(); - } - - // Parse messages - if (json.contains("messages") && json["messages"].isArray()) { - const auto& msgs_arr = json["messages"]; - for (size_t i = 0; i < msgs_arr.size(); ++i) { - const auto& msg_json = msgs_arr[i]; - if (!msg_json.isObject()) - continue; - - Role role = Role::USER; - if (msg_json.contains("role") && msg_json["role"].isString()) { - role = parseRole(msg_json["role"].getString()); - } - - std::string content; - if (msg_json.contains("content") && msg_json["content"].isString()) { - content = msg_json["content"].getString(); - } - - Message msg(role, content); - - if (msg_json.contains("tool_call_id") && - msg_json["tool_call_id"].isString()) { - msg.tool_call_id = msg_json["tool_call_id"].getString(); - } - - if (msg_json.contains("tool_calls") && - msg_json["tool_calls"].isArray()) { - std::vector calls; - const auto& calls_arr = msg_json["tool_calls"]; - for (size_t j = 0; j < calls_arr.size(); ++j) { - const auto& call_json = calls_arr[j]; - if (!call_json.isObject()) - continue; - ToolCall call; - if (call_json.contains("id") && call_json["id"].isString()) { - call.id = call_json["id"].getString(); - } - if (call_json.contains("name") && call_json["name"].isString()) { - call.name = call_json["name"].getString(); - } - if (call_json.contains("arguments")) { - call.arguments = call_json["arguments"]; - } - calls.push_back(std::move(call)); - } - if (!calls.empty()) { - msg.tool_calls = std::move(calls); - } - } - - state.messages.push_back(std::move(msg)); - } - } - - // Parse usage - if (json.contains("usage") && json["usage"].isObject()) { - const auto& usage_json = json["usage"]; - if (usage_json.contains("prompt_tokens") && - usage_json["prompt_tokens"].isNumber()) { - state.total_usage.prompt_tokens = usage_json["prompt_tokens"].getInt(); - } - if (usage_json.contains("completion_tokens") && - usage_json["completion_tokens"].isNumber()) { - state.total_usage.completion_tokens = - usage_json["completion_tokens"].getInt(); - } - if (usage_json.contains("total_tokens") && - usage_json["total_tokens"].isNumber()) { - state.total_usage.total_tokens = usage_json["total_tokens"].getInt(); - } - } - - // Parse error - if (json.contains("error") && json["error"].isObject()) { - const auto& err_json = json["error"]; - int code = 0; - std::string message; - if (err_json.contains("code") && err_json["code"].isNumber()) { - code = err_json["code"].getInt(); - } - if (err_json.contains("message") && err_json["message"].isString()) { - message = err_json["message"].getString(); - } - state.error = Error(code, message); - } - - return state; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// AGENT RESULT -// ═══════════════════════════════════════════════════════════════════════════ - -struct AgentResult { - AgentStatus status = AgentStatus::IDLE; - - // Final response from the agent - std::string response; - - // Full conversation history - std::vector messages; - - // All steps taken - std::vector steps; - - // Total usage across all LLM calls - Usage total_usage; - - // Total time taken - std::chrono::milliseconds duration{0}; - - // Error info (if failed) - optional error; - - // Check if successful - bool isSuccess() const { return status == AgentStatus::COMPLETED; } - - // Get number of iterations - int iterationCount() const { return static_cast(steps.size()); } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// CALLBACKS -// ═══════════════════════════════════════════════════════════════════════════ - -// Called when agent completes -using AgentCallback = std::function)>; - -// Called after each step (for progress monitoring) -using StepCallback = std::function; - -// Called before tool execution (can modify/approve) -using ToolApprovalCallback = std::function; - -// ═══════════════════════════════════════════════════════════════════════════ -// ERROR CODES -// ═══════════════════════════════════════════════════════════════════════════ - -namespace AgentError { -enum : int { - OK = 0, - NO_PROVIDER = -200, - NO_TOOLS = -201, - MAX_ITERATIONS = -202, - TIMEOUT = -203, - TOOL_EXECUTION_FAILED = -204, - LLM_ERROR = -205, - CANCELLED = -206, - UNKNOWN = -299 -}; -} // namespace AgentError - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/api_engine.h b/third_party/gopher-orch/include/gopher/orch/agent/api_engine.h deleted file mode 100644 index 3a808cc1..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/api_engine.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -// ApiEngine - API configuration and utilities for the Agent system -// -// Provides API endpoint configuration that is determined at build time -// based on CMake options and can be queried at runtime. -// -// Usage: -// std::string api_url = ApiEngine::getApiUrlRoot(); - -#include - -namespace gopher { -namespace orch { -namespace agent { - -// ═══════════════════════════════════════════════════════════════════════════ -// API ENGINE -// ═══════════════════════════════════════════════════════════════════════════ - -class ApiEngine { - public: - // ───────────────────────────────────────────────────────────────────────── - // API Configuration - // ───────────────────────────────────────────────────────────────────────── - - // Get the root URL for the Gopher API - // Returns production URL when BUILD_API_PRODUCT=ON, test URL otherwise - static std::string getApiUrlRoot(); - - // ───────────────────────────────────────────────────────────────────────── - // MCP Server Management - // ───────────────────────────────────────────────────────────────────────── - - // Fetch MCP server configurations from the API - // Makes HTTPS request to getApiUrlRoot() + "/v1/mcp-servers" - // Returns JSON response as string - static std::string fetchMcpServers(const std::string& apiKey); - - private: - ApiEngine() = delete; // Static class - no instances -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// INLINE IMPLEMENTATIONS -// ═══════════════════════════════════════════════════════════════════════════ - -inline std::string ApiEngine::getApiUrlRoot() { -#if BUILD_API_PRODUCT - return "https://api.gopher.security"; -#else - return "https://api-test.gopher.security"; -#endif -} - -} // namespace agent -} // namespace orch -} // namespace gopher \ No newline at end of file diff --git a/third_party/gopher-orch/include/gopher/orch/agent/config_loader.h b/third_party/gopher-orch/include/gopher/orch/agent/config_loader.h deleted file mode 100644 index cc1c2be6..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/config_loader.h +++ /dev/null @@ -1,501 +0,0 @@ -#pragma once - -// ConfigLoader - Load tool registry configuration from JSON -// -// Supports: -// - JSON file loading -// - Environment variable substitution (${VAR_NAME}) -// - Parsing of RegistryConfig, ToolDefinition, MCPServerDefinition -// -// Usage: -// ConfigLoader loader; -// loader.setEnv("API_KEY", "secret"); -// -// auto config = loader.loadFromFile("tools.json"); -// if (config.isOk()) { -// registry->loadConfig(config.value(), dispatcher, callback); -// } - -#include -#include -#include -#include - -#include "gopher/orch/agent/tool_definition.h" - -namespace gopher { -namespace orch { -namespace agent { - -// ═══════════════════════════════════════════════════════════════════════════ -// CONFIG LOADER -// ═══════════════════════════════════════════════════════════════════════════ - -class ConfigLoader { - public: - ConfigLoader() = default; - - // ───────────────────────────────────────────────────────────────────────── - // Environment Variables - // ───────────────────────────────────────────────────────────────────────── - - // Set environment variable for ${VAR} substitution - void setEnv(const std::string& name, const std::string& value) { - env_vars_[name] = value; - } - - // Set multiple environment variables - void setEnvMap(const std::map& vars) { - for (const auto& kv : vars) { - env_vars_[kv.first] = kv.second; - } - } - - // Load environment from .env file - VoidResult loadEnvFile(const std::string& path); - - // Substitute ${VAR_NAME} in string - std::string substituteEnvVars(const std::string& input) const { - std::string result = input; - std::regex env_pattern("\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}"); - std::smatch match; - - while (std::regex_search(result, match, env_pattern)) { - std::string var_name = match[1].str(); - std::string value; - - // Check our env vars first - auto it = env_vars_.find(var_name); - if (it != env_vars_.end()) { - value = it->second; - } else { - // Fall back to system env - const char* env_val = std::getenv(var_name.c_str()); - if (env_val) { - value = env_val; - } - } - - result = result.replace(match.position(), match.length(), value); - } - - return result; - } - - // ───────────────────────────────────────────────────────────────────────── - // JSON Loading - // ───────────────────────────────────────────────────────────────────────── - - // Load from file path - Result loadFromFile(const std::string& path); - - // Load from JSON string - Result loadFromString(const std::string& json_string); - - // Load from JsonValue - Result loadFromJson(const JsonValue& json); - - // ───────────────────────────────────────────────────────────────────────── - // Parsing Helpers - // ───────────────────────────────────────────────────────────────────────── - - // Parse individual definitions - Result parseToolDefinition(const JsonValue& json); - Result parseMCPServerDefinition(const JsonValue& json, bool is_api_response = false); - Result parseAuthPreset(const JsonValue& json); - - private: - // Parse HTTP method from string - HttpMethod parseHttpMethod(const std::string& method) const; - - // Parse transport type from string - MCPServerDefinition::TransportType parseTransportType( - const std::string& transport) const; - - std::map env_vars_; -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// INLINE IMPLEMENTATIONS -// ═══════════════════════════════════════════════════════════════════════════ - -inline HttpMethod ConfigLoader::parseHttpMethod( - const std::string& method) const { - if (method == "GET") - return HttpMethod::GET; - if (method == "POST") - return HttpMethod::POST; - if (method == "PUT") - return HttpMethod::PUT; - if (method == "PATCH") - return HttpMethod::PATCH; - if (method == "DELETE") - return HttpMethod::DELETE_; - if (method == "HEAD") - return HttpMethod::HEAD; - if (method == "OPTIONS") - return HttpMethod::OPTIONS; - return HttpMethod::GET; -} - -inline MCPServerDefinition::TransportType ConfigLoader::parseTransportType( - const std::string& transport) const { - if (transport == "stdio") - return MCPServerDefinition::TransportType::STDIO; - if (transport == "http_sse" || transport == "http-sse" || transport == "sse") - return MCPServerDefinition::TransportType::HTTP_SSE; - if (transport == "websocket" || transport == "ws") - return MCPServerDefinition::TransportType::WEBSOCKET; - return MCPServerDefinition::TransportType::STDIO; -} - -inline Result ConfigLoader::parseAuthPreset(const JsonValue& json) { - AuthPreset auth; - - std::string type = - json.contains("type") ? json["type"].getString() : "bearer"; - if (type == "bearer") { - auth.type = AuthPreset::Type::BEARER; - } else if (type == "api_key" || type == "apikey") { - auth.type = AuthPreset::Type::API_KEY; - } else if (type == "basic") { - auth.type = AuthPreset::Type::BASIC; - } - - auth.value = substituteEnvVars( - json.contains("value") ? json["value"].getString() : ""); - auth.header = - json.contains("header") ? json["header"].getString() : "Authorization"; - - return Result(std::move(auth)); -} - -inline Result ConfigLoader::parseMCPServerDefinition( - const JsonValue& json, bool is_api_response) { - MCPServerDefinition def; - - def.name = json.contains("name") ? json["name"].getString() : ""; - if (def.name.empty()) { - return Result( - Error(-1, "MCP server definition missing 'name'")); - } - - std::string transport = - json.contains("transport") ? json["transport"].getString() : "stdio"; - def.transport = parseTransportType(transport); - - // Parse transport-specific config - switch (def.transport) { - case MCPServerDefinition::TransportType::STDIO: { - const JsonValue* stdio_config = nullptr; - if (is_api_response && json.contains("config")) { - stdio_config = &json["config"]; - } else if (json.contains("stdio")) { - stdio_config = &json["stdio"]; - } - - if (stdio_config) { - MCPServerDefinition::StdioConfig cfg; - cfg.command = substituteEnvVars( - stdio_config->contains("command") ? (*stdio_config)["command"].getString() : ""); - - if (stdio_config->contains("args") && (*stdio_config)["args"].isArray()) { - const auto& args = (*stdio_config)["args"]; - for (size_t i = 0; i < args.size(); ++i) { - cfg.args.push_back(substituteEnvVars(args[i].getString())); - } - } - - if (stdio_config->contains("env") && (*stdio_config)["env"].isObject()) { - for (auto it = (*stdio_config)["env"].begin(); it != (*stdio_config)["env"].end(); ++it) { - auto kv = *it; - cfg.env[kv.first] = substituteEnvVars(kv.second.getString()); - } - } - - cfg.working_directory = stdio_config->contains("working_directory") - ? (*stdio_config)["working_directory"].getString() - : ""; - def.stdio_config = std::move(cfg); - } - break; - } - - case MCPServerDefinition::TransportType::HTTP_SSE: { - const JsonValue* sse_config = nullptr; - if (is_api_response && json.contains("config")) { - // New format: use "config" directly for HTTP_SSE - sse_config = &json["config"]; - } else if (json.contains("http_sse")) { - // Old format: nested under "http_sse" - sse_config = &json["http_sse"]; - } - - if (sse_config) { - MCPServerDefinition::HttpSseConfig cfg; - cfg.url = substituteEnvVars(sse_config->contains("url") ? (*sse_config)["url"].getString() - : ""); - cfg.verify_ssl = - sse_config->contains("verify_ssl") ? (*sse_config)["verify_ssl"].getBool() : true; - - if (sse_config->contains("headers") && (*sse_config)["headers"].isObject()) { - for (auto it = (*sse_config)["headers"].begin(); it != (*sse_config)["headers"].end(); - ++it) { - auto kv = *it; - cfg.headers[kv.first] = substituteEnvVars(kv.second.getString()); - } - } - - def.http_sse_config = std::move(cfg); - } - break; - } - - case MCPServerDefinition::TransportType::WEBSOCKET: { - const JsonValue* ws_config = nullptr; - if (is_api_response && json.contains("config")) { - ws_config = &json["config"]; - } else if (json.contains("websocket")) { - ws_config = &json["websocket"]; - } - - if (ws_config) { - MCPServerDefinition::WebSocketConfig cfg; - cfg.url = - substituteEnvVars(ws_config->contains("url") ? (*ws_config)["url"].getString() : ""); - cfg.verify_ssl = - ws_config->contains("verify_ssl") ? (*ws_config)["verify_ssl"].getBool() : true; - - if (ws_config->contains("headers") && (*ws_config)["headers"].isObject()) { - for (auto it = (*ws_config)["headers"].begin(); it != (*ws_config)["headers"].end(); - ++it) { - auto kv = *it; - cfg.headers[kv.first] = substituteEnvVars(kv.second.getString()); - } - } - - def.websocket_config = std::move(cfg); - } - break; - } - } - - // Parse timeouts - handle both old and new field names - if (is_api_response) { - // New format uses camelCase - if (json.contains("connectTimeout")) { - def.connect_timeout = std::chrono::milliseconds(json["connectTimeout"].getInt()); - } - if (json.contains("requestTimeout")) { - def.request_timeout = std::chrono::milliseconds(json["requestTimeout"].getInt()); - } - } else { - // Old format uses snake_case with _ms suffix - if (json.contains("connect_timeout_ms")) { - def.connect_timeout = - std::chrono::milliseconds(json["connect_timeout_ms"].getInt()); - } else if (json.contains("connect_timeout")) { - def.connect_timeout = - std::chrono::milliseconds(json["connect_timeout"].getInt()); - } - if (json.contains("request_timeout_ms")) { - def.request_timeout = - std::chrono::milliseconds(json["request_timeout_ms"].getInt()); - } else if (json.contains("request_timeout")) { - def.request_timeout = - std::chrono::milliseconds(json["request_timeout"].getInt()); - } - } - - if (json.contains("max_retries")) { - def.max_retries = static_cast(json["max_retries"].getInt()); - } - - return Result(std::move(def)); -} - -inline Result ConfigLoader::parseToolDefinition( - const JsonValue& json) { - ToolDefinition def; - - def.name = json.contains("name") ? json["name"].getString() : ""; - if (def.name.empty()) { - return Result(Error(-1, "Tool definition missing 'name'")); - } - - def.description = - json.contains("description") ? json["description"].getString() : ""; - - if (json.contains("input_schema")) { - def.input_schema = json["input_schema"]; - } - - // Parse REST endpoint - if (json.contains("rest_endpoint")) { - const auto& ep = json["rest_endpoint"]; - ToolDefinition::RESTEndpoint rest; - - rest.method = parseHttpMethod( - ep.contains("method") ? ep["method"].getString() : "GET"); - rest.url = - substituteEnvVars(ep.contains("url") ? ep["url"].getString() : ""); - - if (ep.contains("headers") && ep["headers"].isObject()) { - for (auto it = ep["headers"].begin(); it != ep["headers"].end(); ++it) { - auto kv = *it; - rest.headers[kv.first] = substituteEnvVars(kv.second.getString()); - } - } - - if (ep.contains("query_params") && ep["query_params"].isObject()) { - for (auto it = ep["query_params"].begin(); it != ep["query_params"].end(); - ++it) { - auto kv = *it; - rest.query_params[kv.first] = substituteEnvVars(kv.second.getString()); - } - } - - if (ep.contains("path_params") && ep["path_params"].isObject()) { - for (auto it = ep["path_params"].begin(); it != ep["path_params"].end(); - ++it) { - auto kv = *it; - rest.path_params[kv.first] = kv.second.getString(); - } - } - - if (ep.contains("body_mapping") && ep["body_mapping"].isObject()) { - for (auto it = ep["body_mapping"].begin(); it != ep["body_mapping"].end(); - ++it) { - auto kv = *it; - rest.body_mapping[kv.first] = kv.second.getString(); - } - } - - rest.response_path = - ep.contains("response_path") ? ep["response_path"].getString() : ""; - def.rest_endpoint = std::move(rest); - } - - // Parse MCP reference - if (json.contains("mcp_reference")) { - const auto& ref = json["mcp_reference"]; - ToolDefinition::MCPToolRef mcp; - mcp.server_name = - ref.contains("server_name") ? ref["server_name"].getString() : ""; - mcp.tool_name = - ref.contains("tool_name") ? ref["tool_name"].getString() : ""; - def.mcp_reference = std::move(mcp); - } - - // Parse tags - if (json.contains("tags") && json["tags"].isArray()) { - const auto& tags = json["tags"]; - for (size_t i = 0; i < tags.size(); ++i) { - def.tags.push_back(tags[i].getString()); - } - } - - def.require_approval = json.contains("require_approval") - ? json["require_approval"].getBool() - : false; - - return Result(std::move(def)); -} - -inline Result ConfigLoader::loadFromJson( - const JsonValue& json) { - RegistryConfig config; - - // Check if this is a new API response format - bool is_api_response = json.contains("succeeded") && json.contains("data"); - - const JsonValue* config_root = &json; - if (is_api_response) { - // Validate API response format - if (!json["succeeded"].getBool()) { - std::string message = json.contains("message") ? json["message"].getString() : "API request failed"; - return Result(Error(-1, "API Error: " + message)); - } - - if (!json.contains("data") || !json["data"].isObject()) { - return Result(Error(-1, "Invalid API response: missing or invalid 'data' field")); - } - - config_root = &json["data"]; - } - - config.name = config_root->contains("name") ? (*config_root)["name"].getString() : "tool-registry"; - config.base_url = substituteEnvVars( - config_root->contains("base_url") ? (*config_root)["base_url"].getString() : ""); - - // Parse default headers - if (config_root->contains("default_headers") && (*config_root)["default_headers"].isObject()) { - for (auto it = (*config_root)["default_headers"].begin(); - it != (*config_root)["default_headers"].end(); ++it) { - auto kv = *it; - config.default_headers[kv.first] = - substituteEnvVars(kv.second.getString()); - } - } - - // Parse auth presets - if (config_root->contains("auth_presets") && (*config_root)["auth_presets"].isObject()) { - for (auto it = (*config_root)["auth_presets"].begin(); - it != (*config_root)["auth_presets"].end(); ++it) { - auto kv = *it; - auto auth_result = parseAuthPreset(kv.second); - if (mcp::holds_alternative(auth_result)) { - config.auth_presets[kv.first] = mcp::get(auth_result); - } - } - } - - // Parse MCP servers - handle both old and new formats - const JsonValue* servers_array = nullptr; - if (is_api_response && config_root->contains("servers") && (*config_root)["servers"].isArray()) { - // New API response format: data.servers - servers_array = &(*config_root)["servers"]; - } else if (config_root->contains("mcp_servers") && (*config_root)["mcp_servers"].isArray()) { - // Old format: mcp_servers - servers_array = &(*config_root)["mcp_servers"]; - } - - if (servers_array) { - for (size_t i = 0; i < servers_array->size(); ++i) { - auto server_result = parseMCPServerDefinition((*servers_array)[i], is_api_response); - if (mcp::holds_alternative(server_result)) { - config.mcp_servers.push_back( - std::move(mcp::get(server_result))); - } - } - } - - // Parse tools - if (config_root->contains("tools") && (*config_root)["tools"].isArray()) { - const auto& tools = (*config_root)["tools"]; - for (size_t i = 0; i < tools.size(); ++i) { - auto tool_result = parseToolDefinition(tools[i]); - if (mcp::holds_alternative(tool_result)) { - config.tools.push_back( - std::move(mcp::get(tool_result))); - } - } - } - - return Result(std::move(config)); -} - -inline Result ConfigLoader::loadFromString( - const std::string& json_string) { - try { - JsonValue json = JsonValue::parse(json_string); - return loadFromJson(json); - } catch (const std::exception& e) { - return Result( - Error(-1, std::string("JSON parse error: ") + e.what())); - } -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/rest_tool_adapter.h b/third_party/gopher-orch/include/gopher/orch/agent/rest_tool_adapter.h deleted file mode 100644 index aaf45e45..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/rest_tool_adapter.h +++ /dev/null @@ -1,294 +0,0 @@ -#pragma once - -// RESTToolAdapter - Create tools from REST endpoint definitions -// -// Converts ToolDefinition with RESTEndpoint to executable tools. -// Supports: -// - Path parameter substitution (/users/{id}) -// - Query parameter mapping ($.field) -// - Request body mapping -// - Response path extraction -// - Environment variable substitution - -#include -#include -#include -#include -#include - -#include "gopher/orch/agent/tool_definition.h" -#include "gopher/orch/server/rest_server.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::server; - -// Tool execution function signature (also defined in tool_registry.h) -using ToolFunction = std::function; - -// ═══════════════════════════════════════════════════════════════════════════ -// JSON PATH UTILITIES -// ═══════════════════════════════════════════════════════════════════════════ - -// Extract value from JSON using simple path ($.field.subfield) -inline JsonValue extractJsonPath(const JsonValue& json, - const std::string& path) { - if (path.empty() || path == "$") { - return json; - } - - // Remove leading "$." if present - std::string clean_path = path; - if (clean_path.substr(0, 2) == "$.") { - clean_path = clean_path.substr(2); - } else if (clean_path[0] == '$') { - clean_path = clean_path.substr(1); - } - - // Split by dots and traverse - JsonValue current = json; - std::istringstream iss(clean_path); - std::string token; - - while (std::getline(iss, token, '.')) { - if (token.empty()) - continue; - - // Check for array index [n] - auto bracket_pos = token.find('['); - if (bracket_pos != std::string::npos) { - std::string field = token.substr(0, bracket_pos); - std::string index_str = token.substr(bracket_pos + 1); - index_str.pop_back(); // Remove ] - - if (!field.empty()) { - if (!current.contains(field)) { - return JsonValue(); - } - current = current[field]; - } - - int index = std::stoi(index_str); - if (!current.isArray() || index >= static_cast(current.size())) { - return JsonValue(); - } - current = current[index]; - } else { - if (!current.isObject() || !current.contains(token)) { - return JsonValue(); - } - current = current[token]; - } - } - - return current; -} - -// Extract value as string -inline std::string extractJsonPathString(const JsonValue& json, - const std::string& path) { - JsonValue value = extractJsonPath(json, path); - if (value.isNull()) { - return ""; - } - if (value.isString()) { - return value.getString(); - } - return value.toString(); -} - -// URL encode string -inline std::string urlEncode(const std::string& str) { - std::string encoded; - for (char c : str) { - if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { - encoded += c; - } else { - char hex[4]; - std::snprintf(hex, sizeof(hex), "%%%02X", static_cast(c)); - encoded += hex; - } - } - return encoded; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// REST TOOL ADAPTER -// ═══════════════════════════════════════════════════════════════════════════ - -class RESTToolAdapter { - public: - explicit RESTToolAdapter(HttpClientPtr http_client = nullptr) - : http_client_(http_client ? http_client - : std::make_shared()) {} - - // Set default headers for all requests - void setDefaultHeaders(const std::map& headers) { - default_headers_ = headers; - } - - // Set base URL for relative paths - void setBaseUrl(const std::string& url) { base_url_ = url; } - - // Set environment variable for substitution - void setEnv(const std::string& name, const std::string& value) { - env_vars_[name] = value; - } - - // Create a tool function from REST endpoint definition - ToolFunction createToolFunction(const ToolDefinition& def) { - if (!def.rest_endpoint) { - return nullptr; - } - - const auto& endpoint = *def.rest_endpoint; - - return [this, endpoint](const JsonValue& input, Dispatcher& dispatcher, - JsonCallback callback) { - executeRESTCall(endpoint, input, dispatcher, std::move(callback)); - }; - } - - // Execute a REST call directly - void executeRESTCall(const ToolDefinition::RESTEndpoint& endpoint, - const JsonValue& input, - Dispatcher& dispatcher, - JsonCallback callback) { - // Build URL - std::string url = substituteEnvVars(endpoint.url); - - // Add base URL if path is relative - if (!url.empty() && url[0] == '/') { - url = base_url_ + url; - } - - // Substitute path parameters - for (const auto& kv : endpoint.path_params) { - std::string value = extractJsonPathString(input, kv.second); - std::regex param_regex("\\{" + kv.first + "\\}"); - url = std::regex_replace(url, param_regex, urlEncode(value)); - } - - // Build query string - if (!endpoint.query_params.empty()) { - bool has_query = url.find('?') != std::string::npos; - for (const auto& kv : endpoint.query_params) { - std::string value = - substituteEnvVars(extractJsonPathString(input, kv.second)); - if (!value.empty()) { - url += (has_query ? "&" : "?"); - url += urlEncode(kv.first) + "=" + urlEncode(value); - has_query = true; - } - } - } - - // Build headers - std::map headers = default_headers_; - for (const auto& kv : endpoint.headers) { - headers[kv.first] = substituteEnvVars(kv.second); - } - if (headers.find("Content-Type") == headers.end()) { - headers["Content-Type"] = "application/json"; - } - - // Build body for POST/PUT/PATCH - std::string body; - if (endpoint.method == HttpMethod::POST || - endpoint.method == HttpMethod::PUT || - endpoint.method == HttpMethod::PATCH) { - if (!endpoint.body_mapping.empty()) { - JsonValue body_json = JsonValue::object(); - for (const auto& kv : endpoint.body_mapping) { - body_json[kv.first] = extractJsonPath(input, kv.second); - } - body = body_json.toString(); - } else { - body = input.toString(); - } - } - - // Make request - http_client_->request( - endpoint.method, url, headers, body, dispatcher, - [endpoint, - callback = std::move(callback)](Result result) { - if (!mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - return; - } - - auto& response = mcp::get(result); - if (!response.isSuccess()) { - callback(Result( - Error(-1, "HTTP " + std::to_string(response.status_code) + - ": " + response.body))); - return; - } - - // Parse response - JsonValue json; - try { - if (!response.body.empty()) { - json = JsonValue::parse(response.body); - } else { - json = JsonValue::object(); - } - } catch (...) { - // If not JSON, wrap as string - json = response.body; - } - - // Extract with path if specified - if (!endpoint.response_path.empty()) { - json = extractJsonPath(json, endpoint.response_path); - } - - callback(Result(std::move(json))); - }); - } - - private: - std::string substituteEnvVars(const std::string& input) const { - std::string result = input; - std::regex env_pattern("\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}"); - std::smatch match; - - while (std::regex_search(result, match, env_pattern)) { - std::string var_name = match[1].str(); - std::string value; - - auto it = env_vars_.find(var_name); - if (it != env_vars_.end()) { - value = it->second; - } else { - const char* env_val = std::getenv(var_name.c_str()); - if (env_val) { - value = env_val; - } - } - - result = result.replace(match.position(), match.length(), value); - } - - return result; - } - - HttpClientPtr http_client_; - std::map default_headers_; - std::string base_url_; - std::map env_vars_; -}; - -using RESTToolAdapterPtr = std::shared_ptr; - -inline RESTToolAdapterPtr makeRESTToolAdapter(HttpClientPtr client = nullptr) { - return std::make_shared(client); -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/tool_definition.h b/third_party/gopher-orch/include/gopher/orch/agent/tool_definition.h deleted file mode 100644 index 49e1b6c7..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/tool_definition.h +++ /dev/null @@ -1,354 +0,0 @@ -#pragma once - -// Tool Definition Types - Configuration-driven tool definitions -// -// Provides structured types for defining tools from: -// - REST API endpoints -// - MCP server references -// - Lambda functions -// -// Supports JSON configuration with environment variable substitution. - -#include -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" -#include "gopher/orch/llm/llm_types.h" -#include "gopher/orch/server/rest_server.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; -using namespace gopher::orch::llm; -using namespace gopher::orch::server; - -// ═══════════════════════════════════════════════════════════════════════════ -// TOOL DEFINITION - Unified tool configuration -// ═══════════════════════════════════════════════════════════════════════════ - -struct ToolDefinition { - std::string name; - std::string description; - JsonValue input_schema; // JSON Schema for parameters - - // ───────────────────────────────────────────────────────────────────────── - // Option 1: REST Endpoint - // ───────────────────────────────────────────────────────────────────────── - struct RESTEndpoint { - HttpMethod method = HttpMethod::GET; - std::string url; // Full URL or path (supports ${ENV_VAR}) - std::map headers; - - // Parameter mapping (JSONPath-like expressions: $.field) - std::map query_params; // {"q": "$.query"} - std::map path_params; // {"id": "$.user_id"} - std::map body_mapping; // For POST body - - // Response extraction - std::string response_path; // JSONPath to extract from response - - RESTEndpoint() = default; - }; - optional rest_endpoint; - - // ───────────────────────────────────────────────────────────────────────── - // Option 2: MCP Server Reference - // ───────────────────────────────────────────────────────────────────────── - struct MCPToolRef { - std::string server_name; // Name of registered MCP server - std::string tool_name; // Tool name on that server - - MCPToolRef() = default; - MCPToolRef(const std::string& server, const std::string& tool) - : server_name(server), tool_name(tool) {} - }; - optional mcp_reference; - - // ───────────────────────────────────────────────────────────────────────── - // Option 3: Lambda/Function (programmatic only) - // ───────────────────────────────────────────────────────────────────────── - using Handler = - std::function; - optional handler; - - // Metadata - std::vector tags; - bool require_approval = false; // Human-in-the-loop - - ToolDefinition() = default; - - // Builder pattern - ToolDefinition& withName(const std::string& n) { - name = n; - return *this; - } - - ToolDefinition& withDescription(const std::string& desc) { - description = desc; - return *this; - } - - ToolDefinition& withInputSchema(const JsonValue& schema) { - input_schema = schema; - return *this; - } - - ToolDefinition& withRESTEndpoint(const RESTEndpoint& ep) { - rest_endpoint = ep; - return *this; - } - - ToolDefinition& withMCPReference(const std::string& server, - const std::string& tool) { - mcp_reference = MCPToolRef(server, tool); - return *this; - } - - ToolDefinition& withHandler(Handler h) { - handler = std::move(h); - return *this; - } - - ToolDefinition& withTag(const std::string& tag) { - tags.push_back(tag); - return *this; - } - - ToolDefinition& withApprovalRequired(bool required = true) { - require_approval = required; - return *this; - } - - // Convert to ToolSpec for LLM - ToolSpec toToolSpec() const { - ToolSpec spec; - spec.name = name; - spec.description = description; - spec.parameters = input_schema; - return spec; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// MCP SERVER DEFINITION - Remote MCP server configuration -// ═══════════════════════════════════════════════════════════════════════════ - -struct MCPServerDefinition { - std::string name; - - enum class TransportType { STDIO, HTTP_SSE, WEBSOCKET }; - TransportType transport = TransportType::STDIO; - - // STDIO transport - struct StdioConfig { - std::string command; - std::vector args; - std::map env; - std::string working_directory; - - StdioConfig() = default; - StdioConfig(const std::string& cmd, - const std::vector& arguments = {}) - : command(cmd), args(arguments) {} - }; - optional stdio_config; - - // HTTP-SSE transport - struct HttpSseConfig { - std::string url; - std::map headers; - bool verify_ssl = true; - - HttpSseConfig() = default; - explicit HttpSseConfig(const std::string& u) : url(u) {} - }; - optional http_sse_config; - - // WebSocket transport - struct WebSocketConfig { - std::string url; - std::map headers; - bool verify_ssl = true; - - WebSocketConfig() = default; - explicit WebSocketConfig(const std::string& u) : url(u) {} - }; - optional websocket_config; - - // Connection settings - std::chrono::milliseconds connect_timeout{30000}; - std::chrono::milliseconds request_timeout{60000}; - uint32_t max_retries = 3; - - MCPServerDefinition() = default; - explicit MCPServerDefinition(const std::string& n) : name(n) {} - - // Builder pattern for STDIO - static MCPServerDefinition stdio(const std::string& name, - const std::string& command, - const std::vector& args = {}) { - MCPServerDefinition def(name); - def.transport = TransportType::STDIO; - def.stdio_config = StdioConfig(command, args); - return def; - } - - // Builder pattern for HTTP-SSE - static MCPServerDefinition httpSse(const std::string& name, - const std::string& url) { - MCPServerDefinition def(name); - def.transport = TransportType::HTTP_SSE; - def.http_sse_config = HttpSseConfig(url); - return def; - } - - // Builder pattern for WebSocket - static MCPServerDefinition websocket(const std::string& name, - const std::string& url) { - MCPServerDefinition def(name); - def.transport = TransportType::WEBSOCKET; - def.websocket_config = WebSocketConfig(url); - return def; - } - - MCPServerDefinition& withEnv(const std::string& key, - const std::string& value) { - if (stdio_config) { - stdio_config->env[key] = value; - } - return *this; - } - - MCPServerDefinition& withHeader(const std::string& key, - const std::string& value) { - if (http_sse_config) { - http_sse_config->headers[key] = value; - } else if (websocket_config) { - websocket_config->headers[key] = value; - } - return *this; - } - - MCPServerDefinition& withTimeout(std::chrono::milliseconds timeout) { - connect_timeout = timeout; - request_timeout = timeout; - return *this; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// AUTH PRESET - Reusable authentication configuration -// ═══════════════════════════════════════════════════════════════════════════ - -struct AuthPreset { - enum class Type { BEARER, API_KEY, BASIC }; - Type type = Type::BEARER; - - std::string value; // Token/key (supports ${ENV_VAR}) - std::string header = "Authorization"; // Header name for API_KEY - - AuthPreset() = default; - - static AuthPreset bearer(const std::string& token) { - AuthPreset auth; - auth.type = Type::BEARER; - auth.value = token; - return auth; - } - - static AuthPreset apiKey(const std::string& key, - const std::string& header_name = "X-API-Key") { - AuthPreset auth; - auth.type = Type::API_KEY; - auth.value = key; - auth.header = header_name; - return auth; - } - - static AuthPreset basic(const std::string& credentials) { - AuthPreset auth; - auth.type = Type::BASIC; - auth.value = credentials; - return auth; - } - - // Build header value - std::string headerValue() const { - switch (type) { - case Type::BEARER: - return "Bearer " + value; - case Type::BASIC: - return "Basic " + value; - case Type::API_KEY: - return value; - default: - return value; - } - } - - // Get header name - std::string headerName() const { - if (type == Type::API_KEY) { - return header; - } - return "Authorization"; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// REGISTRY CONFIG - Complete configuration file structure -// ═══════════════════════════════════════════════════════════════════════════ - -struct RegistryConfig { - std::string name = "tool-registry"; - std::string base_url; // Default base URL for REST tools - std::map default_headers; - - // Authentication presets (reusable) - std::map auth_presets; - - // MCP servers to connect - std::vector mcp_servers; - - // Tool definitions - std::vector tools; - - RegistryConfig() = default; - explicit RegistryConfig(const std::string& n) : name(n) {} - - // Builder pattern - RegistryConfig& withBaseUrl(const std::string& url) { - base_url = url; - return *this; - } - - RegistryConfig& withHeader(const std::string& key, const std::string& value) { - default_headers[key] = value; - return *this; - } - - RegistryConfig& withAuthPreset(const std::string& name, - const AuthPreset& auth) { - auth_presets[name] = auth; - return *this; - } - - RegistryConfig& withMCPServer(const MCPServerDefinition& server) { - mcp_servers.push_back(server); - return *this; - } - - RegistryConfig& withTool(const ToolDefinition& tool) { - tools.push_back(tool); - return *this; - } -}; - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/tool_executor.h b/third_party/gopher-orch/include/gopher/orch/agent/tool_executor.h deleted file mode 100644 index 7be5c295..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/tool_executor.h +++ /dev/null @@ -1,156 +0,0 @@ -#pragma once - -// ToolExecutor - Executes tools from a ToolRegistry -// -// Separates execution concerns from the registry: -// - ToolRegistry: stores and retrieves tool definitions -// - ToolExecutor: looks up and executes tools -// -// Usage: -// auto registry = makeToolRegistry(); -// registry->addTool("calculator", "Perform calculations", schema, handler); -// -// auto executor = makeToolExecutor(registry); -// executor->executeTool("calculator", args, dispatcher, callback); - -#include -#include -#include -#include - -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/core/types.h" -#include "gopher/orch/llm/llm_types.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; -using namespace gopher::orch::llm; - -// Forward declaration -class ToolExecutor; -using ToolExecutorPtr = std::shared_ptr; - -// ToolExecutor - Executes tools by looking them up in a registry -// -// Thread Safety: -// - All execution methods are thread-safe -// - Callbacks are invoked in the dispatcher thread context -class ToolExecutor { - public: - using Ptr = std::shared_ptr; - - explicit ToolExecutor(ToolRegistryPtr registry) - : registry_(std::move(registry)) {} - ~ToolExecutor() = default; - - // Factory - static Ptr create(ToolRegistryPtr registry) { - return std::make_shared(std::move(registry)); - } - - // Get the underlying registry - ToolRegistryPtr registry() const { return registry_; } - - // ═══════════════════════════════════════════════════════════════════════════ - // TOOL EXECUTION - // ═══════════════════════════════════════════════════════════════════════════ - - // Execute a tool by name - void executeTool(const std::string& name, - const JsonValue& arguments, - Dispatcher& dispatcher, - JsonCallback callback) { - if (!registry_) { - dispatcher.post([callback = std::move(callback)]() { - callback(Result(Error(-1, "No registry configured"))); - }); - return; - } - - auto entry_opt = registry_->getToolEntry(name); - if (!entry_opt.has_value()) { - dispatcher.post([callback = std::move(callback), name]() { - callback(Result(Error(-1, "Tool not found: " + name))); - }); - return; - } - - const auto& entry = entry_opt.value(); - - if (entry.isLocal()) { - // Execute local function - entry.function(arguments, dispatcher, std::move(callback)); - } else { - // Try composite first if available (unified execution path) - auto composite = registry_->getServerComposite(); - if (composite) { - auto tool = composite->tool(name); - if (tool) { - RunnableConfig config; - tool->invoke(arguments, config, dispatcher, std::move(callback)); - return; - } - } - - // Fallback: direct server call (backward compat when no composite) - RunnableConfig config; - std::string tool_name = - entry.original_name.empty() ? entry.spec.name : entry.original_name; - - entry.server->callTool(tool_name, arguments, config, dispatcher, - std::move(callback)); - } - } - - // Execute a ToolCall (convenience method) - void executeToolCall(const ToolCall& call, - Dispatcher& dispatcher, - JsonCallback callback) { - executeTool(call.name, call.arguments, dispatcher, std::move(callback)); - } - - // Execute multiple tool calls (optionally in parallel) - void executeToolCalls( - const std::vector& calls, - bool parallel, - Dispatcher& dispatcher, - std::function>)> callback) { - if (calls.empty()) { - dispatcher.post([callback = std::move(callback)]() { callback({}); }); - return; - } - - auto results = - std::make_shared>>(calls.size()); - auto pending = std::make_shared>(calls.size()); - - for (size_t i = 0; i < calls.size(); ++i) { - executeToolCall( - calls[i], dispatcher, - [results, pending, i, callback](Result result) { - (*results)[i] = std::move(result); - if (--(*pending) == 0) { - callback(std::move(*results)); - } - }); - - // Note: True sequential execution would require callback chaining - // This implementation executes all calls and collects results - } - } - - private: - ToolRegistryPtr registry_; -}; - -// Convenience function to create executor -inline ToolExecutorPtr makeToolExecutor(ToolRegistryPtr registry) { - return ToolExecutor::create(std::move(registry)); -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/tool_registry.h b/third_party/gopher-orch/include/gopher/orch/agent/tool_registry.h deleted file mode 100644 index 12342023..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/tool_registry.h +++ /dev/null @@ -1,522 +0,0 @@ -#pragma once - -// ToolRegistry - Tool repository for agents -// -// Stores and retrieves tools from multiple sources: -// - Local lambda functions -// - MCP servers (via Server interface) -// - REST endpoints (via JSON config) -// - JSON configuration files -// -// This is a pure repository - for execution, use ToolExecutor. -// -// Usage: -// auto registry = makeToolRegistry(); -// -// // Option 1: Load from JSON config -// registry->loadFromFile("tools.json", dispatcher, callback); -// -// // Option 2: Add tools programmatically -// registry->addTool("calculator", "Perform calculations", schema, handler); -// -// // Option 3: Add from MCP server -// registry->addServer(mcpServer); -// -// // Get specs for LLM -// auto specs = registry->getToolSpecs(); -// -// // For execution, use ToolExecutor: -// auto executor = makeToolExecutor(registry); -// executor->executeTool("calculator", args, dispatcher, callback); - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" -#include "gopher/orch/llm/llm_types.h" -#include "gopher/orch/server/server.h" -#include "gopher/orch/server/server_composite.h" - -// Forward declarations for config loading -namespace gopher { -namespace orch { -namespace agent { -struct ToolDefinition; -struct MCPServerDefinition; -struct RegistryConfig; -class ConfigLoader; -class RESTToolAdapter; -} // namespace agent -} // namespace orch -} // namespace gopher - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; -using namespace gopher::orch::llm; -using namespace gopher::orch::server; - -// Forward declaration -class ToolRegistry; -using ToolRegistryPtr = std::shared_ptr; - -// Tool execution function signature -using ToolFunction = std::function; - -// ═══════════════════════════════════════════════════════════════════════════ -// CONVERSION UTILITIES -// ═══════════════════════════════════════════════════════════════════════════ - -// Convert ServerToolInfo (from Server) to ToolSpec (for LLM) -inline ToolSpec toToolSpec(const ServerToolInfo& info) { - ToolSpec spec; - spec.name = info.name; - spec.description = info.description; - spec.parameters = info.inputSchema; - return spec; -} - -// Convert ToolSpec (from LLM) to ServerToolInfo (for Server) -inline ServerToolInfo toServerToolInfo(const ToolSpec& spec) { - ServerToolInfo info; - info.name = spec.name; - info.description = spec.description; - info.inputSchema = spec.parameters; - return info; -} - -// Internal tool entry -struct ToolEntry { - ToolSpec spec; - ToolFunction function; - ServerPtr server; // nullptr for local tools - std::string - original_name; // Original name on server (may differ from spec.name) - - bool isLocal() const { return server == nullptr; } - bool isRemote() const { return server != nullptr; } -}; - -// ToolRegistry - Tool repository for agents -// -// Thread Safety: -// - Configuration methods (addTool, addServer) should be called before use -// - Read methods (getToolSpecs, getToolEntry) are thread-safe after -// configuration -class ToolRegistry { - public: - using Ptr = std::shared_ptr; - - ToolRegistry() = default; - ~ToolRegistry() = default; - - // Factory - static Ptr create() { return std::make_shared(); } - - // ═══════════════════════════════════════════════════════════════════════════ - // SERVER COMPOSITE DELEGATION - // ═══════════════════════════════════════════════════════════════════════════ - - // Set the underlying ServerComposite for delegated server operations - void setServerComposite(ServerCompositePtr composite) { - std::lock_guard lock(mutex_); - server_composite_ = std::move(composite); - } - - // Get the underlying ServerComposite - ServerCompositePtr getServerComposite() const { - std::lock_guard lock(mutex_); - return server_composite_; - } - - // ═══════════════════════════════════════════════════════════════════════════ - // LOCAL TOOLS - // ═══════════════════════════════════════════════════════════════════════════ - - // Add a local tool with lambda function - void addTool(const std::string& name, - const std::string& description, - const JsonValue& parameters, - ToolFunction function) { - std::lock_guard lock(mutex_); - - ToolEntry entry; - entry.spec.name = name; - entry.spec.description = description; - entry.spec.parameters = parameters; - entry.function = std::move(function); - entry.server = nullptr; - - tools_[name] = std::move(entry); - } - - // Add a local tool with ToolSpec - void addTool(const ToolSpec& spec, ToolFunction function) { - addTool(spec.name, spec.description, spec.parameters, std::move(function)); - } - - // Add a synchronous tool (wraps in async callback) - void addSyncTool( - const std::string& name, - const std::string& description, - const JsonValue& parameters, - std::function(const JsonValue&)> function) { - addTool(name, description, parameters, - [func = std::move(function)](const JsonValue& args, - Dispatcher& dispatcher, - JsonCallback callback) { - auto result = func(args); - dispatcher.post([callback = std::move(callback), - result = std::move(result)]() { - callback(std::move(result)); - }); - }); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // REMOTE TOOLS (MCP/REST Servers) - // ═══════════════════════════════════════════════════════════════════════════ - - // Add all tools from a server (async - fetches tool list) - void addServer(ServerPtr server, Dispatcher& dispatcher) { - if (!server) - return; - - // Store server reference - { - std::lock_guard lock(mutex_); - servers_.push_back(server); - } - - // Capture composite for lambda (may be nullptr) - ServerCompositePtr composite; - { - std::lock_guard lock(mutex_); - composite = server_composite_; - } - - // List and register tools - server->listTools( - dispatcher, [this, server, composite](Result> result) { - if (!mcp::holds_alternative>(result)) - return; - - auto tools = mcp::get>(result); - - // If composite is set, delegate server registration to it - if (composite) { - std::vector tool_names; - tool_names.reserve(tools.size()); - for (const auto& t : tools) { - tool_names.push_back(t.name); - } - // Add to composite without namespacing (ToolRegistry handles its own naming) - composite->addServer(server, tool_names, false); - } - - // Register tool specs for LLM (execution goes through composite if available) - std::lock_guard lock(mutex_); - for (const auto& info : tools) { - ToolEntry entry; - entry.spec = toToolSpec(info); // Use conversion utility - entry.server = server; - entry.original_name = info.name; - - // Use prefixed name to avoid conflicts - std::string prefixed_key = server->name() + ":" + info.name; - tools_[prefixed_key] = entry; - - // Also register without prefix if no conflict - if (tools_.find(info.name) == tools_.end()) { - tools_[info.name] = entry; - } - } - }); - } - - // Add all tools from a server (sync - provide tool list directly) - void addServer(ServerPtr server, const std::vector& tools) { - if (!server) - return; - - std::lock_guard lock(mutex_); - servers_.push_back(server); - - // If composite is set, delegate server registration to it - if (server_composite_) { - std::vector tool_names; - tool_names.reserve(tools.size()); - for (const auto& t : tools) { - tool_names.push_back(t.name); - } - // Add to composite without namespacing (ToolRegistry handles its own naming) - server_composite_->addServer(server, tool_names, false); - } - - // Register tool specs for LLM - for (const auto& info : tools) { - ToolEntry entry; - entry.spec = toToolSpec(info); - entry.server = server; - entry.original_name = info.name; - - std::string prefixed_key = server->name() + ":" + info.name; - tools_[prefixed_key] = entry; - - if (tools_.find(info.name) == tools_.end()) { - tools_[info.name] = entry; - } - } - } - - // Add specific tool from a server with ServerToolInfo - void addServerTool(ServerPtr server, - const ServerToolInfo& info, - const std::string& alias = "") { - if (!server) - return; - - std::lock_guard lock(mutex_); - - ToolEntry entry; - entry.spec = toToolSpec(info); - if (!alias.empty()) { - entry.spec.name = alias; // Override name with alias - } - entry.server = server; - entry.original_name = info.name; - - std::string key = alias.empty() ? info.name : alias; - tools_[key] = std::move(entry); - } - - // Add specific tool from a server by name (spec fetched later) - void addServerTool(ServerPtr server, - const std::string& tool_name, - const std::string& alias = "") { - if (!server) - return; - - std::lock_guard lock(mutex_); - - ToolEntry entry; - entry.spec.name = alias.empty() ? tool_name : alias; - entry.server = server; - entry.original_name = tool_name; - - std::string key = alias.empty() ? tool_name : alias; - tools_[key] = std::move(entry); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TOOL ACCESS - // ═══════════════════════════════════════════════════════════════════════════ - - // Get tool specs for LLM - std::vector getToolSpecs() const { - std::lock_guard lock(mutex_); - - std::vector specs; - std::set seen_names; // Track unique tool names - - // Return unique tools by spec.name - // Prefer non-prefixed entries over prefixed ones - for (const auto& pair : tools_) { - const auto& spec = pair.second.spec; - - // Skip if we've already seen a tool with this name - if (seen_names.find(spec.name) != seen_names.end()) { - continue; - } - - // Skip prefixed entries if the non-prefixed version exists - // (Prefixed entries have ':' in the map key) - if (pair.first.find(':') != std::string::npos) { - // Check if non-prefixed version exists - auto it = tools_.find(spec.name); - if (it != tools_.end()) { - continue; // Skip this prefixed entry - } - } - - specs.push_back(spec); - seen_names.insert(spec.name); - } - - return specs; - } - - // Get a specific tool's spec - optional getToolSpec(const std::string& name) const { - std::lock_guard lock(mutex_); - auto it = tools_.find(name); - if (it == tools_.end()) { - return nullopt; - } - return it->second.spec; - } - - // Get tool entry (for advanced usage) - optional getToolEntry(const std::string& name) const { - std::lock_guard lock(mutex_); - auto it = tools_.find(name); - if (it == tools_.end()) { - return nullopt; - } - return it->second; - } - - // Check if tool exists - bool hasTool(const std::string& name) const { - std::lock_guard lock(mutex_); - return tools_.find(name) != tools_.end(); - } - - // Get tool names - std::vector getToolNames() const { - std::lock_guard lock(mutex_); - - std::vector names; - names.reserve(tools_.size()); - - for (const auto& pair : tools_) { - names.push_back(pair.first); - } - - return names; - } - - // Get tool count (unique tools only, excluding duplicate registrations) - size_t toolCount() const { - std::lock_guard lock(mutex_); - - std::set seen_names; // Track unique tool names - - // Count unique tools by spec.name (same logic as getToolSpecs) - for (const auto& pair : tools_) { - const auto& spec = pair.second.spec; - - // Skip if we've already seen a tool with this name - if (seen_names.find(spec.name) != seen_names.end()) { - continue; - } - - // Skip prefixed entries if the non-prefixed version exists - if (pair.first.find(':') != std::string::npos) { - auto it = tools_.find(spec.name); - if (it != tools_.end()) { - continue; // Skip this prefixed entry - } - } - - seen_names.insert(spec.name); - } - - return seen_names.size(); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // MANAGEMENT - // ═══════════════════════════════════════════════════════════════════════════ - - // Remove a tool - void removeTool(const std::string& name) { - std::lock_guard lock(mutex_); - tools_.erase(name); - } - - // Clear all tools - void clear() { - std::lock_guard lock(mutex_); - tools_.clear(); - servers_.clear(); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // CONFIG LOADING (requires tool_definition.h, config_loader.h, - // rest_tool_adapter.h) - // ═══════════════════════════════════════════════════════════════════════════ - - // Load from JSON config file - // Requires: #include "gopher/orch/agent/config_loader.h" - // #include "gopher/orch/agent/rest_tool_adapter.h" - void loadFromFile(const std::string& path, - Dispatcher& dispatcher, - std::function callback); - - // Load from JSON string - void loadFromString(const std::string& json_string, - Dispatcher& dispatcher, - std::function callback); - - // Load from RegistryConfig struct - void loadConfig(const RegistryConfig& config, - Dispatcher& dispatcher, - std::function callback); - - // Register a tool from ToolDefinition - VoidResult registerTool(const ToolDefinition& def, Dispatcher& dispatcher); - - // ═══════════════════════════════════════════════════════════════════════════ - // ENVIRONMENT VARIABLES - // ═══════════════════════════════════════════════════════════════════════════ - - // Set environment variable for ${VAR} substitution - void setEnv(const std::string& name, const std::string& value) { - std::lock_guard lock(mutex_); - env_vars_[name] = value; - } - - // Load environment from .env file - VoidResult loadEnvFile(const std::string& path); - - // ═══════════════════════════════════════════════════════════════════════════ - // MCP SERVER MANAGEMENT (for config loading) - // ═══════════════════════════════════════════════════════════════════════════ - - // Add MCP server from definition - void addMCPServer(const MCPServerDefinition& def, - Dispatcher& dispatcher, - std::function callback); - - // Get registered MCP server by name - ServerPtr getMCPServer(const std::string& name) const { - std::lock_guard lock(mutex_); - auto it = mcp_servers_.find(name); - return it != mcp_servers_.end() ? it->second : nullptr; - } - - // List registered MCP server names - std::vector getMCPServerNames() const { - std::lock_guard lock(mutex_); - std::vector names; - for (const auto& kv : mcp_servers_) { - names.push_back(kv.first); - } - return names; - } - - private: - mutable std::mutex mutex_; - std::map tools_; - std::vector servers_; - std::map mcp_servers_; // Named MCP servers - std::map env_vars_; // Environment variables - ServerCompositePtr server_composite_; // Delegate server tool operations -}; - -// Convenience function to create registry -inline ToolRegistryPtr makeToolRegistry() { return ToolRegistry::create(); } - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/tool_runnable.h b/third_party/gopher-orch/include/gopher/orch/agent/tool_runnable.h deleted file mode 100644 index d826bf3e..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/tool_runnable.h +++ /dev/null @@ -1,128 +0,0 @@ -#pragma once - -// ToolRunnable - Wraps ToolExecutor as a composable Runnable -// -// Enables tool execution to be composed with other Runnables in pipelines, -// sequences, and graphs. Supports both single tool calls and parallel -// execution of multiple tool calls. -// -// Usage: -// auto registry = makeToolRegistry(); -// registry->addTool("search", "Search the web", schema, handler); -// auto executor = makeToolExecutor(registry); -// auto tool_runnable = ToolRunnable::create(executor); -// -// JsonValue input = JsonValue::object(); -// input["name"] = "search"; -// input["arguments"] = args; -// -// tool_runnable->invoke(input, config, dispatcher, callback); - -#include -#include - -#include "gopher/orch/agent/tool_executor.h" -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; - -// ToolRunnable - Adapter that makes ToolExecutor a Runnable -// -// Input Schema (single tool call): -// { -// "id": "call_123", // optional, used for result mapping -// "name": "search", -// "arguments": {...} -// } -// -// Input Schema (multiple tool calls - parallel execution): -// { -// "tool_calls": [ -// {"id": "call_1", "name": "search", "arguments": {...}}, -// {"id": "call_2", "name": "calculator", "arguments": {...}} -// ] -// } -// -// Output Schema (single): -// { -// "id": "call_123", -// "result": {...}, -// "success": true -// } -// -// Output Schema (multiple): -// { -// "results": [ -// {"id": "call_1", "result": {...}, "success": true}, -// {"id": "call_2", "result": 4, "success": true} -// ] -// } -class ToolRunnable : public Runnable { - public: - using Ptr = std::shared_ptr; - - // Factory method - static Ptr create(ToolExecutorPtr executor); - - // Runnable interface - std::string name() const override; - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override; - - // Accessors - ToolExecutorPtr executor() const { return executor_; } - ToolRegistryPtr registry() const { - return executor_ ? executor_->registry() : nullptr; - } - - private: - explicit ToolRunnable(ToolExecutorPtr executor); - - // Execute a single tool call - void executeSingle(const std::string& id, - const std::string& name, - const JsonValue& arguments, - Dispatcher& dispatcher, - Callback callback); - - // Execute multiple tool calls in parallel - void executeMultiple(const std::vector& calls, - Dispatcher& dispatcher, - Callback callback); - - // Parse single tool call from input - struct SingleCall { - std::string id; - std::string name; - JsonValue arguments; - bool valid = false; - }; - static SingleCall parseSingleCall(const JsonValue& input); - - // Parse multiple tool calls from input - static std::vector parseMultipleCalls(const JsonValue& input); - - ToolExecutorPtr executor_; -}; - -// Convenience factory function -inline ToolRunnable::Ptr makeToolRunnable(ToolExecutorPtr executor) { - return ToolRunnable::create(std::move(executor)); -} - -// Create ToolRunnable directly from registry -inline ToolRunnable::Ptr makeToolRunnable(ToolRegistryPtr registry) { - return ToolRunnable::create(makeToolExecutor(std::move(registry))); -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/agent/tools_fetcher.h b/third_party/gopher-orch/include/gopher/orch/agent/tools_fetcher.h deleted file mode 100644 index 130d6361..00000000 --- a/third_party/gopher-orch/include/gopher/orch/agent/tools_fetcher.h +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once - -/** - * @file tools_fetcher.h - * @brief ToolsFetcher - Thin orchestration layer for JSON-to-Agent pipeline - * - * This class coordinates existing components to load tool configurations - * from JSON and create a ToolRegistry backed by ServerComposite. - */ - -#include -#include -#include - -#include "gopher/orch/core/types.h" - -// Forward declarations -namespace gopher { -namespace orch { - -namespace server { -class ServerComposite; -class MCPServer; -} // namespace server - -namespace agent { - -class ConfigLoader; -class ToolRegistry; - -using namespace gopher::orch::core; - -/** - * @class ToolsFetcher - * @brief Orchestrates tool loading from JSON configuration - * - * ToolsFetcher is a thin layer that: - * 1. Loads JSON configuration using ConfigLoader - * 2. Creates MCP servers from the configuration - * 3. Aggregates servers in a ServerComposite - * 4. Wraps the composite in a ToolRegistry for agent use - */ -class ToolsFetcher { - public: - ToolsFetcher() = default; - ~ToolsFetcher() = default; - - /** - * @brief Load tools from JSON configuration string - * @param json_config JSON configuration containing mcp_servers array - * @param dispatcher Event dispatcher for async operations - * @param callback Called when loading completes or fails - */ - void loadFromJson(const std::string& json_config, - Dispatcher& dispatcher, - std::function callback); - - /** - * @brief Load tools from JSON configuration file - * @param file_path Path to JSON configuration file - * @param dispatcher Event dispatcher for async operations - * @param callback Called when loading completes or fails - */ - void loadFromFile(const std::string& file_path, - Dispatcher& dispatcher, - std::function callback); - - /** - * @brief Get the configured ToolRegistry - * @return Shared pointer to ToolRegistry, nullptr if not loaded - */ - std::shared_ptr getRegistry() const { return registry_; } - - /** - * @brief Get the underlying ServerComposite - * @return Shared pointer to ServerComposite, nullptr if not loaded - */ - std::shared_ptr getComposite() const { - return composite_; - } - - /** - * @brief Shutdown all connections (SSE connections are long-lived) - * @param dispatcher Event dispatcher for async operations - * @param callback Called when shutdown completes - * - * This must be called before program exit to close SSE connections, - * otherwise the event loop will keep running indefinitely. - */ - void shutdown(Dispatcher& dispatcher, std::function callback); - - private: - std::shared_ptr config_loader_; - std::shared_ptr composite_; - std::shared_ptr registry_; -}; - -} // namespace agent -} // namespace orch -} // namespace gopher \ No newline at end of file diff --git a/third_party/gopher-orch/include/gopher/orch/callback/callback_handler.h b/third_party/gopher-orch/include/gopher/orch/callback/callback_handler.h deleted file mode 100644 index eecfc470..00000000 --- a/third_party/gopher-orch/include/gopher/orch/callback/callback_handler.h +++ /dev/null @@ -1,292 +0,0 @@ -#pragma once - -// CallbackHandler - Interface for receiving observability events -// -// Provides hooks for monitoring execution of chains, tools, and custom events. -// Implementations can log, trace, or perform other observability tasks. -// -// All handler methods have default empty implementations, allowing handlers -// to override only the events they care about. - -#include -#include -#include - -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { -namespace callback { - -// ============================================================================= -// EventType - Categories of observable events -// ============================================================================= - -enum class EventType { - CHAIN_START, // Runnable chain begins execution - CHAIN_END, // Runnable chain completes successfully - CHAIN_ERROR, // Runnable chain fails with error - TOOL_START, // Tool invocation begins - TOOL_END, // Tool invocation completes successfully - TOOL_ERROR, // Tool invocation fails with error - LLM_START, // LLM request begins (future use) - LLM_END, // LLM request completes (future use) - LLM_ERROR, // LLM request fails (future use) - CUSTOM // User-defined custom event -}; - -// ============================================================================= -// RunInfo - Contextual information about a running operation -// ============================================================================= - -// RunInfo carries metadata about the current execution context. -// This information flows through the callback chain, enabling: -// - Hierarchical tracing via parent_run_id -// - Timing measurements via start_time -// - Filtering and grouping via tags -// - Custom context via metadata -struct RunInfo { - std::string run_id; // Unique identifier for this run - std::string parent_run_id; // Parent run ID for hierarchical tracing - std::string name; // Human-readable name of the operation - std::string run_type; // Type: "chain", "tool", "llm", "graph", etc. - std::chrono::steady_clock::time_point start_time; // When execution started - std::vector tags; // Tags for filtering - core::JsonValue metadata; // Additional metadata - - RunInfo() - : start_time(std::chrono::steady_clock::now()), - metadata(core::JsonValue::object()) {} - - // Calculate duration from start to now - std::chrono::milliseconds durationMs() const { - auto now = std::chrono::steady_clock::now(); - return std::chrono::duration_cast(now - - start_time); - } -}; - -// ============================================================================= -// CallbackHandler - Interface for receiving events -// ============================================================================= - -// CallbackHandler is the base interface for all callback handlers. -// Implementations override the event methods they want to handle. -// Default implementations are provided (empty) so handlers only need -// to implement what they care about. -// -// All callback methods are called synchronously in the dispatcher thread. -// Handlers should not block or perform expensive operations. -class CallbackHandler { - public: - virtual ~CallbackHandler() = default; - - // ------------------------------------------------------------------------- - // Chain Events - Fired for Runnable chain execution - // ------------------------------------------------------------------------- - - // Called when a chain (sequence of runnables) starts execution - virtual void onChainStart(const RunInfo& info, const core::JsonValue& input) { - (void)info; - (void)input; - } - - // Called when a chain completes successfully - virtual void onChainEnd(const RunInfo& info, const core::JsonValue& output) { - (void)info; - (void)output; - } - - // Called when a chain fails with an error - virtual void onChainError(const RunInfo& info, const core::Error& error) { - (void)info; - (void)error; - } - - // ------------------------------------------------------------------------- - // Tool Events - Fired for tool/server invocations - // ------------------------------------------------------------------------- - - // Called when a tool invocation starts - virtual void onToolStart(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& input) { - (void)info; - (void)tool_name; - (void)input; - } - - // Called when a tool invocation completes successfully - virtual void onToolEnd(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& output) { - (void)info; - (void)tool_name; - (void)output; - } - - // Called when a tool invocation fails with an error - virtual void onToolError(const RunInfo& info, - const std::string& tool_name, - const core::Error& error) { - (void)info; - (void)tool_name; - (void)error; - } - - // ------------------------------------------------------------------------- - // LLM Events - For future LLM integration - // ------------------------------------------------------------------------- - - // Called when an LLM request starts - virtual void onLLMStart(const RunInfo& info, const core::JsonValue& input) { - (void)info; - (void)input; - } - - // Called when an LLM request completes - virtual void onLLMEnd(const RunInfo& info, const core::JsonValue& output) { - (void)info; - (void)output; - } - - // Called when an LLM request fails - virtual void onLLMError(const RunInfo& info, const core::Error& error) { - (void)info; - (void)error; - } - - // ------------------------------------------------------------------------- - // Custom Events - User-defined events - // ------------------------------------------------------------------------- - - // Called for user-defined custom events - // event_name: Identifies the event type (e.g., "fsm.transition") - // data: Event-specific payload - virtual void onCustomEvent(const std::string& event_name, - const core::JsonValue& data) { - (void)event_name; - (void)data; - } - - // ------------------------------------------------------------------------- - // Retry Events - For resilience pattern observability - // ------------------------------------------------------------------------- - - // Called when a retry is about to be attempted - virtual void onRetry(const RunInfo& info, - const core::Error& error, - uint32_t attempt, - uint32_t max_attempts) { - (void)info; - (void)error; - (void)attempt; - (void)max_attempts; - } -}; - -// ============================================================================= -// LoggingCallbackHandler - Logs events for debugging -// ============================================================================= - -// LoggingCallbackHandler provides a simple logging implementation. -// By default, it uses a simple stdout-based logging. In production, -// you would typically use a proper logging framework. -class LoggingCallbackHandler : public CallbackHandler { - public: - // Log level for filtering messages - enum class LogLevel { DEBUG, INFO, WARN, ERROR }; - - explicit LoggingCallbackHandler(LogLevel min_level = LogLevel::INFO) - : min_level_(min_level) {} - - void onChainStart(const RunInfo& info, - const core::JsonValue& input) override { - log(LogLevel::INFO, "CHAIN_START", info.name, input); - } - - void onChainEnd(const RunInfo& info, const core::JsonValue& output) override { - log(LogLevel::INFO, "CHAIN_END", - info.name + " (" + std::to_string(info.durationMs().count()) + "ms)", - output); - } - - void onChainError(const RunInfo& info, const core::Error& error) override { - logError(LogLevel::ERROR, "CHAIN_ERROR", info.name, error); - } - - void onToolStart(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& input) override { - log(LogLevel::INFO, "TOOL_START", tool_name, input); - } - - void onToolEnd(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& output) override { - log(LogLevel::INFO, "TOOL_END", - tool_name + " (" + std::to_string(info.durationMs().count()) + "ms)", - output); - } - - void onToolError(const RunInfo& info, - const std::string& tool_name, - const core::Error& error) override { - logError(LogLevel::ERROR, "TOOL_ERROR", tool_name, error); - } - - void onCustomEvent(const std::string& event_name, - const core::JsonValue& data) override { - log(LogLevel::DEBUG, "CUSTOM", event_name, data); - } - - void onRetry(const RunInfo& info, - const core::Error& error, - uint32_t attempt, - uint32_t max_attempts) override { - std::string msg = info.name + " attempt " + std::to_string(attempt) + "/" + - std::to_string(max_attempts); - logError(LogLevel::WARN, "RETRY", msg, error); - } - - protected: - // Override these methods to integrate with your logging framework - virtual void log(LogLevel level, - const std::string& event, - const std::string& name, - const core::JsonValue& data) { - if (level < min_level_) { - return; - } - // Simple stdout logging - replace with proper logging in production - printf("[%s] %s - %s\n", event.c_str(), name.c_str(), - data.toString().c_str()); - } - - virtual void logError(LogLevel level, - const std::string& event, - const std::string& name, - const core::Error& error) { - if (level < min_level_) { - return; - } - printf("[%s] %s - %s (code: %d)\n", event.c_str(), name.c_str(), - error.message.c_str(), error.code); - } - - private: - LogLevel min_level_; -}; - -// ============================================================================= -// NoOpCallbackHandler - Does nothing (for testing/disabling callbacks) -// ============================================================================= - -class NoOpCallbackHandler : public CallbackHandler { - public: - // All methods use default empty implementations -}; - -} // namespace callback -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/callback/callback_manager.h b/third_party/gopher-orch/include/gopher/orch/callback/callback_manager.h deleted file mode 100644 index 40b64ad2..00000000 --- a/third_party/gopher-orch/include/gopher/orch/callback/callback_manager.h +++ /dev/null @@ -1,483 +0,0 @@ -#pragma once - -// CallbackManager - Manages callback handlers and emits events -// -// The CallbackManager is responsible for: -// 1. Maintaining a collection of callback handlers -// 2. Emitting events to all registered handlers -// 3. Managing run context (run IDs, parent relationships) -// 4. Creating child managers for nested operations -// -// Usage: -// auto manager = std::make_shared(); -// manager->addHandler(std::make_shared()); -// -// // Start a chain -// auto run_info = manager->startChain("my_chain", input); -// // ... execute chain ... -// manager->endChain(run_info, output); - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/callback/callback_handler.h" -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { -namespace callback { - -// ============================================================================= -// CallbackManager - Manages callback handlers -// ============================================================================= - -// CallbackManager is thread-safe and can be shared across multiple operations. -// It manages the lifecycle of run contexts and emits events to all handlers. -// -// Hierarchical tracing is supported through parent_run_id relationships: -// - When creating a child manager, the parent's run_id becomes the child's -// parent_run_id -// - This allows reconstruction of the full execution tree -class CallbackManager : public std::enable_shared_from_this { - public: - using Ptr = std::shared_ptr; - - CallbackManager() : run_id_(generateRunId()), parent_run_id_("") {} - - // ------------------------------------------------------------------------- - // Handler Management - // ------------------------------------------------------------------------- - - // Add a handler to receive events - void addHandler(std::shared_ptr handler) { - std::lock_guard lock(mutex_); - handlers_.push_back(std::move(handler)); - } - - // Remove a handler - void removeHandler(const std::shared_ptr& handler) { - std::lock_guard lock(mutex_); - handlers_.erase(std::remove(handlers_.begin(), handlers_.end(), handler), - handlers_.end()); - } - - // Get the number of registered handlers - size_t handlerCount() const { - std::lock_guard lock(mutex_); - return handlers_.size(); - } - - // Clear all handlers - void clearHandlers() { - std::lock_guard lock(mutex_); - handlers_.clear(); - } - - // ------------------------------------------------------------------------- - // Run Context Management - // ------------------------------------------------------------------------- - - // Get the current run ID - const std::string& runId() const { return run_id_; } - - // Get the parent run ID (empty if this is the root) - const std::string& parentRunId() const { return parent_run_id_; } - - // Set the parent run ID (used when creating child managers) - void setParentRunId(const std::string& parent_id) { - parent_run_id_ = parent_id; - } - - // ------------------------------------------------------------------------- - // Chain Event Emission - // ------------------------------------------------------------------------- - - // Start a chain and emit CHAIN_START event - // Returns RunInfo that should be passed to endChain/errorChain - RunInfo startChain( - const std::string& name, - const core::JsonValue& input, - const std::vector& tags = {}, - const core::JsonValue& metadata = core::JsonValue::object()) { - RunInfo info = createRunInfo(name, "chain", tags, metadata); - emitChainStart(info, input); - return info; - } - - // End a chain successfully and emit CHAIN_END event - void endChain(const RunInfo& info, const core::JsonValue& output) { - emitChainEnd(info, output); - } - - // End a chain with error and emit CHAIN_ERROR event - void errorChain(const RunInfo& info, const core::Error& error) { - emitChainError(info, error); - } - - // ------------------------------------------------------------------------- - // Tool Event Emission - // ------------------------------------------------------------------------- - - // Start a tool invocation and emit TOOL_START event - RunInfo startTool( - const std::string& tool_name, - const core::JsonValue& input, - const std::vector& tags = {}, - const core::JsonValue& metadata = core::JsonValue::object()) { - RunInfo info = createRunInfo(tool_name, "tool", tags, metadata); - emitToolStart(info, tool_name, input); - return info; - } - - // End a tool invocation successfully and emit TOOL_END event - void endTool(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& output) { - emitToolEnd(info, tool_name, output); - } - - // End a tool invocation with error and emit TOOL_ERROR event - void errorTool(const RunInfo& info, - const std::string& tool_name, - const core::Error& error) { - emitToolError(info, tool_name, error); - } - - // ------------------------------------------------------------------------- - // LLM Event Emission (for future use) - // ------------------------------------------------------------------------- - - RunInfo startLLM( - const std::string& name, - const core::JsonValue& input, - const std::vector& tags = {}, - const core::JsonValue& metadata = core::JsonValue::object()) { - RunInfo info = createRunInfo(name, "llm", tags, metadata); - emitLLMStart(info, input); - return info; - } - - void endLLM(const RunInfo& info, const core::JsonValue& output) { - emitLLMEnd(info, output); - } - - void errorLLM(const RunInfo& info, const core::Error& error) { - emitLLMError(info, error); - } - - // ------------------------------------------------------------------------- - // Direct Event Emission (lower-level API) - // ------------------------------------------------------------------------- - - void emitChainStart(const RunInfo& info, const core::JsonValue& input) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onChainStart(info, input); - } - } - - void emitChainEnd(const RunInfo& info, const core::JsonValue& output) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onChainEnd(info, output); - } - } - - void emitChainError(const RunInfo& info, const core::Error& error) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onChainError(info, error); - } - } - - void emitToolStart(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& input) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onToolStart(info, tool_name, input); - } - } - - void emitToolEnd(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& output) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onToolEnd(info, tool_name, output); - } - } - - void emitToolError(const RunInfo& info, - const std::string& tool_name, - const core::Error& error) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onToolError(info, tool_name, error); - } - } - - void emitLLMStart(const RunInfo& info, const core::JsonValue& input) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onLLMStart(info, input); - } - } - - void emitLLMEnd(const RunInfo& info, const core::JsonValue& output) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onLLMEnd(info, output); - } - } - - void emitLLMError(const RunInfo& info, const core::Error& error) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onLLMError(info, error); - } - } - - // Emit a custom event - void emitCustomEvent(const std::string& event_name, - const core::JsonValue& data) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onCustomEvent(event_name, data); - } - } - - // Emit a retry event - void emitRetry(const RunInfo& info, - const core::Error& error, - uint32_t attempt, - uint32_t max_attempts) { - std::lock_guard lock(mutex_); - for (const auto& handler : handlers_) { - handler->onRetry(info, error, attempt, max_attempts); - } - } - - // ------------------------------------------------------------------------- - // Child Manager Creation - // ------------------------------------------------------------------------- - - // Create a child manager for nested operations. - // The child inherits all handlers and sets up parent-child tracing. - // - // Usage: - // auto child = manager->child(); - // auto info = child->startChain("nested_chain", input); - // // info.parent_run_id will be set to parent's run_id - Ptr child() { - auto child_manager = std::make_shared(); - child_manager->parent_run_id_ = run_id_; - - // Copy handlers (share the same handler instances) - std::lock_guard lock(mutex_); - child_manager->handlers_ = handlers_; - - return child_manager; - } - - // Create a child manager with a specific name for the child run - Ptr childWithName(const std::string& name) { - auto child_manager = child(); - child_manager->run_name_ = name; - return child_manager; - } - - // ------------------------------------------------------------------------- - // Tag and Metadata Management - // ------------------------------------------------------------------------- - - // Add inheritable tags that will be passed to child managers - void addTags(const std::vector& tags) { - std::lock_guard lock(mutex_); - inheritable_tags_.insert(inheritable_tags_.end(), tags.begin(), tags.end()); - } - - // Add inheritable metadata that will be passed to child managers - void addMetadata(const std::string& key, const core::JsonValue& value) { - std::lock_guard lock(mutex_); - inheritable_metadata_[key] = value; - } - - // Get current inheritable tags - std::vector inheritableTags() const { - std::lock_guard lock(mutex_); - return inheritable_tags_; - } - - // Get current inheritable metadata - core::JsonValue inheritableMetadata() const { - std::lock_guard lock(mutex_); - return inheritable_metadata_; - } - - private: - // Generate a unique run ID - // Uses a simple counter + random component for uniqueness - static std::string generateRunId() { - static std::atomic counter{0}; - uint64_t count = counter.fetch_add(1); - - // Generate random component - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution dis(0, 0xFFFFFFFF); - uint32_t random_part = dis(gen); - - std::ostringstream oss; - oss << "run-" << std::hex << count << "-" << random_part; - return oss.str(); - } - - // Create a RunInfo with current context - RunInfo createRunInfo(const std::string& name, - const std::string& run_type, - const std::vector& tags, - const core::JsonValue& metadata) { - RunInfo info; - info.run_id = generateRunId(); - info.parent_run_id = parent_run_id_; - info.name = name; - info.run_type = run_type; - - // Combine inheritable tags with provided tags - { - std::lock_guard lock(mutex_); - info.tags = inheritable_tags_; - } - info.tags.insert(info.tags.end(), tags.begin(), tags.end()); - - // Merge inheritable metadata with provided metadata - info.metadata = inheritableMetadata(); - if (metadata.isObject()) { - for (auto it = metadata.begin(); it != metadata.end(); ++it) { - auto kv = *it; - info.metadata[kv.first] = kv.second; - } - } - - return info; - } - - mutable std::mutex mutex_; - std::vector> handlers_; - std::string run_id_; - std::string parent_run_id_; - std::string run_name_; - std::vector inheritable_tags_; - core::JsonValue inheritable_metadata_{core::JsonValue::object()}; -}; - -// ============================================================================= -// RAII Guard for automatic chain lifecycle management -// ============================================================================= - -// ChainGuard automatically ends a chain when it goes out of scope. -// This ensures that chain events are properly closed even if an exception -// is thrown or early return occurs. -// -// Usage: -// { -// ChainGuard guard(manager, "my_chain", input); -// // ... do work ... -// guard.setOutput(output); // Mark successful completion -// } // Automatically calls endChain or errorChain -class ChainGuard { - public: - ChainGuard(CallbackManager::Ptr manager, - const std::string& name, - const core::JsonValue& input) - : manager_(std::move(manager)), completed_(false) { - run_info_ = manager_->startChain(name, input); - } - - ~ChainGuard() { - if (!completed_) { - // If not explicitly completed, treat as error - manager_->errorChain( - run_info_, - core::Error(core::OrchError::INTERNAL_ERROR, "Chain not completed")); - } - } - - // Mark the chain as successfully completed - void setOutput(const core::JsonValue& output) { - manager_->endChain(run_info_, output); - completed_ = true; - } - - // Mark the chain as failed with an error - void setError(const core::Error& error) { - manager_->errorChain(run_info_, error); - completed_ = true; - } - - // Get the run info for this chain - const RunInfo& runInfo() const { return run_info_; } - - // Prevent copying - ChainGuard(const ChainGuard&) = delete; - ChainGuard& operator=(const ChainGuard&) = delete; - - private: - CallbackManager::Ptr manager_; - RunInfo run_info_; - bool completed_; -}; - -// ============================================================================= -// RAII Guard for automatic tool lifecycle management -// ============================================================================= - -class ToolGuard { - public: - ToolGuard(CallbackManager::Ptr manager, - const std::string& tool_name, - const core::JsonValue& input) - : manager_(std::move(manager)), tool_name_(tool_name), completed_(false) { - run_info_ = manager_->startTool(tool_name, input); - } - - ~ToolGuard() { - if (!completed_) { - manager_->errorTool( - run_info_, tool_name_, - core::Error(core::OrchError::INTERNAL_ERROR, "Tool not completed")); - } - } - - void setOutput(const core::JsonValue& output) { - manager_->endTool(run_info_, tool_name_, output); - completed_ = true; - } - - void setError(const core::Error& error) { - manager_->errorTool(run_info_, tool_name_, error); - completed_ = true; - } - - const RunInfo& runInfo() const { return run_info_; } - - ToolGuard(const ToolGuard&) = delete; - ToolGuard& operator=(const ToolGuard&) = delete; - - private: - CallbackManager::Ptr manager_; - std::string tool_name_; - RunInfo run_info_; - bool completed_; -}; - -} // namespace callback -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/composition/parallel.h b/third_party/gopher-orch/include/gopher/orch/composition/parallel.h deleted file mode 100644 index fe3fb1e2..00000000 --- a/third_party/gopher-orch/include/gopher/orch/composition/parallel.h +++ /dev/null @@ -1,183 +0,0 @@ -#pragma once - -// Parallel - Execute multiple runnables concurrently -// Distributes the same input to all branches, collects results into a map -// -// Behavior: -// - All branches receive the same input -// - Branches execute concurrently (subject to dispatcher threading) -// - Results collected into a JSON object with branch keys -// - Fails fast: first error cancels pending branches (TODO: make configurable) - -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace composition { - -using namespace gopher::orch::core; - -// Parallel execution of JSON runnables -// Input is distributed to all branches, results collected by key -class Parallel : public JsonRunnable { - public: - using Callback = JsonRunnable::Callback; - - explicit Parallel(const std::string& name = "Parallel") : name_(name) {} - - // Add a named branch - Parallel& add(const std::string& key, JsonRunnablePtr runnable) { - branches_.emplace_back(key, std::move(runnable)); - return *this; - } - - std::string name() const override { - if (!name_.empty() && name_ != "Parallel") { - return name_; - } - if (branches_.empty()) { - return "Parallel(empty)"; - } - std::string result = "Parallel("; - for (size_t i = 0; i < branches_.size(); ++i) { - if (i > 0) - result += ", "; - result += branches_[i].first; - } - result += ")"; - return result; - } - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - if (branches_.empty()) { - // Empty parallel returns empty object - dispatcher.post([callback = std::move(callback)]() { - callback(makeSuccess(JsonValue::object())); - }); - return; - } - - // Shared state for collecting results from all branches - auto state = - std::make_shared(branches_.size(), std::move(callback)); - - // Launch all branches concurrently - for (size_t i = 0; i < branches_.size(); ++i) { - const auto& key = branches_[i].first; - const auto& runnable = branches_[i].second; - - runnable->invoke(input, config.child(), dispatcher, - [state, key, &dispatcher](Result result) { - state->onBranchComplete(key, std::move(result), - dispatcher); - }); - } - } - - // Get number of branches - size_t size() const { return branches_.size(); } - - // Check if empty - bool empty() const { return branches_.empty(); } - - private: - // State shared across all branch callbacks - struct ParallelState { - ParallelState(size_t total, Callback callback) - : remaining(total), - failed(false), - callback_(std::move(callback)), - results_(JsonValue::object()) {} - - void onBranchComplete(const std::string& key, - Result result, - Dispatcher& dispatcher) { - std::lock_guard lock(mutex_); - - // Skip if already failed (fail-fast mode) - if (failed) { - return; - } - - if (mcp::holds_alternative(result)) { - // First error triggers callback - failed = true; - // Post to dispatcher to ensure callback runs in dispatcher context - auto cb = std::move(callback_); - auto error = mcp::get(result); - dispatcher.post( - [cb = std::move(cb), error]() { cb(Result(error)); }); - return; - } - - // Store successful result - results_[key] = mcp::get(result); - remaining--; - - if (remaining == 0) { - // All branches completed successfully - auto cb = std::move(callback_); - auto results = std::move(results_); - dispatcher.post([cb = std::move(cb), results = std::move(results)]() { - cb(makeSuccess(std::move(results))); - }); - } - } - - std::mutex mutex_; - size_t remaining; - bool failed; - Callback callback_; - JsonValue results_; - }; - - std::vector> branches_; - std::string name_; -}; - -// Builder for creating Parallel with fluent API -class ParallelBuilder { - public: - explicit ParallelBuilder(const std::string& name = "Parallel") - : parallel_(std::make_shared(name)) {} - - ParallelBuilder& add(const std::string& key, JsonRunnablePtr runnable) { - parallel_->add(key, std::move(runnable)); - return *this; - } - - // Template version for typed runnables - template - ParallelBuilder& add(const std::string& key, std::shared_ptr runnable) { - parallel_->add(key, - std::static_pointer_cast(std::move(runnable))); - return *this; - } - - std::shared_ptr build() { return std::move(parallel_); } - - // Implicit conversion to shared_ptr - operator std::shared_ptr() { return build(); } - - private: - std::shared_ptr parallel_; -}; - -// Factory for Parallel -inline ParallelBuilder parallel(const std::string& name = "Parallel") { - return ParallelBuilder(name); -} - -} // namespace composition -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/composition/router.h b/third_party/gopher-orch/include/gopher/orch/composition/router.h deleted file mode 100644 index 31dc45f6..00000000 --- a/third_party/gopher-orch/include/gopher/orch/composition/router.h +++ /dev/null @@ -1,147 +0,0 @@ -#pragma once - -// Router - Conditional branching for runnables -// Routes input to different runnables based on conditions -// -// Behavior: -// - Evaluates conditions in order until one matches -// - Routes to the matching runnable -// - Falls back to default if no condition matches -// - Returns error if no match and no default - -#include -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace composition { - -using namespace gopher::orch::core; - -// Type-safe Router for typed runnables -// Evaluates conditions against input and routes to matching runnable -template -class Router : public Runnable { - public: - using Condition = std::function; - using RunnablePtr = std::shared_ptr>; - using Route = std::pair; - using Callback = typename Runnable::Callback; - - Router(std::vector routes, - RunnablePtr default_route, - const std::string& name = "") - : routes_(std::move(routes)), - default_(std::move(default_route)), - name_(name) {} - - std::string name() const override { - if (!name_.empty()) { - return name_; - } - std::string result = "Router("; - result += std::to_string(routes_.size()) + " routes"; - if (default_) { - result += ", default=" + default_->name(); - } - result += ")"; - return result; - } - - void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Evaluate conditions in order - for (const auto& route : routes_) { - if (route.first(input)) { - // Found matching route - invoke it - route.second->invoke(input, config.child(), dispatcher, - std::move(callback)); - return; - } - } - - // No match - try default route - if (default_) { - default_->invoke(input, config.child(), dispatcher, std::move(callback)); - return; - } - - // No match and no default - return error - dispatcher.post([callback = std::move(callback)]() { - callback(makeOrchError(OrchError::INVALID_ARGUMENT, - "No matching route and no default")); - }); - } - - // Get number of routes - size_t size() const { return routes_.size(); } - - // Check if has default route - bool hasDefault() const { return default_ != nullptr; } - - private: - std::vector routes_; - RunnablePtr default_; - std::string name_; -}; - -// JSON Router - type-erased version for dynamic routing -using JsonRouter = Router; - -// Builder for creating Router with fluent API -template -class RouterBuilder { - public: - using Condition = std::function; - using RunnablePtr = std::shared_ptr>; - - explicit RouterBuilder(const std::string& name = "") : name_(name) {} - - // Add a conditional route - RouterBuilder& when(Condition condition, RunnablePtr runnable) { - routes_.emplace_back(std::move(condition), std::move(runnable)); - return *this; - } - - // Set default route (when no conditions match) - RouterBuilder& otherwise(RunnablePtr runnable) { - default_ = std::move(runnable); - return *this; - } - - std::shared_ptr> build() { - return std::make_shared>(std::move(routes_), - std::move(default_), name_); - } - - // Implicit conversion to shared_ptr - operator std::shared_ptr>() { return build(); } - - private: - std::vector> routes_; - RunnablePtr default_; - std::string name_; -}; - -// Factory for JSON router builder -inline RouterBuilder router( - const std::string& name = "") { - return RouterBuilder(name); -} - -// Factory function for type-safe router -template -RouterBuilder makeRouter(const std::string& name = "") { - return RouterBuilder(name); -} - -} // namespace composition -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/composition/sequence.h b/third_party/gopher-orch/include/gopher/orch/composition/sequence.h deleted file mode 100644 index 3327b7a8..00000000 --- a/third_party/gopher-orch/include/gopher/orch/composition/sequence.h +++ /dev/null @@ -1,207 +0,0 @@ -#pragma once - -// Sequence - Chain runnables together: output of one becomes input of next -// Implements the pipe pattern: A | B | C means A.output -> B.input -> C.input -// -// Short-circuits on first error - subsequent steps are not executed - -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace composition { - -using namespace gopher::orch::core; - -// Sequence of two runnables with type-safe chaining -// A's output must match B's input type -template -class Sequence2 : public Runnable { - public: - using FirstPtr = std::shared_ptr>; - using SecondPtr = std::shared_ptr>; - using Callback = typename Runnable::Callback; - - Sequence2(FirstPtr first, SecondPtr second, const std::string& name = "") - : first_(std::move(first)), - second_(std::move(second)), - name_(name.empty() ? first_->name() + " | " + second_->name() : name) {} - - std::string name() const override { return name_; } - - void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Capture pointers by value to extend lifetime - auto first = first_; - auto second = second_; - - // Invoke first, then chain to second on success - first->invoke(input, config, dispatcher, - [second, config, &dispatcher, callback = std::move(callback)]( - Result result) mutable { - if (mcp::holds_alternative(result)) { - // Short-circuit: propagate error without running second - callback(Result(mcp::get(result))); - } else { - // Chain: use first's output as second's input - second->invoke(mcp::get(result), config.child(), - dispatcher, std::move(callback)); - } - }); - } - - private: - FirstPtr first_; - SecondPtr second_; - std::string name_; -}; - -// JSON Sequence - chains multiple JSON runnables -// Uses type-erased JsonRunnable for dynamic composition -class Sequence : public JsonRunnable { - public: - using Callback = JsonRunnable::Callback; - - explicit Sequence(const std::string& name = "Sequence") : name_(name) {} - - // Add a step to the sequence - Sequence& add(JsonRunnablePtr step) { - steps_.push_back(std::move(step)); - return *this; - } - - // Build the sequence name from step names if not explicitly set - std::string name() const override { - if (!name_.empty() && name_ != "Sequence") { - return name_; - } - if (steps_.empty()) { - return "Sequence(empty)"; - } - std::string result = steps_[0]->name(); - for (size_t i = 1; i < steps_.size(); ++i) { - result += " | " + steps_[i]->name(); - } - return result; - } - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - if (steps_.empty()) { - // Empty sequence just passes through input - dispatcher.post([input, callback = std::move(callback)]() { - callback(makeSuccess(input)); - }); - return; - } - - // Start the chain with first step - invokeStep(0, input, config, dispatcher, std::move(callback)); - } - - // Get number of steps - size_t size() const { return steps_.size(); } - - // Check if empty - bool empty() const { return steps_.empty(); } - - private: - void invokeStep(size_t index, - const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) { - if (index >= steps_.size()) { - // All steps completed successfully - dispatcher.post([input, callback = std::move(callback)]() { - callback(makeSuccess(input)); - }); - return; - } - - // Capture state for the callback chain - // Using shared_from_this to keep the Sequence alive during async execution - auto self = std::static_pointer_cast(this->shared_from_this()); - auto step = steps_[index]; - - step->invoke( - input, config.child(), dispatcher, - [self, index, config, &dispatcher, - callback = std::move(callback)](Result result) mutable { - if (mcp::holds_alternative(result)) { - // Short-circuit on error - callback(std::move(result)); - } else { - // Continue to next step - self->invokeStep(index + 1, mcp::get(result), config, - dispatcher, std::move(callback)); - } - }); - } - - std::vector steps_; - std::string name_; -}; - -// Builder for creating Sequence with fluent API -class SequenceBuilder { - public: - explicit SequenceBuilder(const std::string& name = "Sequence") - : sequence_(std::make_shared(name)) {} - - SequenceBuilder& add(JsonRunnablePtr step) { - sequence_->add(std::move(step)); - return *this; - } - - // Template version for typed runnables - template - SequenceBuilder& add(std::shared_ptr step) { - sequence_->add(std::static_pointer_cast(std::move(step))); - return *this; - } - - std::shared_ptr build() { return std::move(sequence_); } - - // Implicit conversion to shared_ptr - operator std::shared_ptr() { return build(); } - - private: - std::shared_ptr sequence_; -}; - -// Factory function for type-safe two-step sequence -template -std::shared_ptr> makeSequence( - std::shared_ptr> first, - std::shared_ptr> second, - const std::string& name = "") { - return std::make_shared>(std::move(first), - std::move(second), name); -} - -// Operator | for chaining (type-safe version) -template -std::shared_ptr> operator|( - std::shared_ptr> first, - std::shared_ptr> second) { - return makeSequence(std::move(first), std::move(second)); -} - -// Factory for JSON sequence -inline SequenceBuilder sequence(const std::string& name = "Sequence") { - return SequenceBuilder(name); -} - -} // namespace composition -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/core/config.h b/third_party/gopher-orch/include/gopher/orch/core/config.h deleted file mode 100644 index e3906930..00000000 --- a/third_party/gopher-orch/include/gopher/orch/core/config.h +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once - -// RunnableConfig - Configuration options for Runnable invocations -// Provides metadata, tags, and execution options that flow through the chain - -#include -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { - -// Forward declaration for CallbackManager (avoids circular dependency) -namespace callback { -class CallbackManager; -} // namespace callback - -namespace core { - -// Configuration passed to each Runnable invocation -// Carries metadata, tags, and execution options through the composition chain -class RunnableConfig { - public: - RunnableConfig() = default; - - // Builder pattern for fluent configuration - RunnableConfig& withTag(const std::string& key, const std::string& value) { - tags_[key] = value; - return *this; - } - - RunnableConfig& withMetadata(const std::string& key, const JsonValue& value) { - metadata_[key] = value; - return *this; - } - - RunnableConfig& withRunName(const std::string& name) { - run_name_ = name; - return *this; - } - - RunnableConfig& withMaxConcurrency(size_t max) { - max_concurrency_ = max; - return *this; - } - - RunnableConfig& withTimeout(std::chrono::milliseconds timeout) { - timeout_ms_ = timeout; - return *this; - } - - RunnableConfig& withRecursionLimit(size_t limit) { - recursion_limit_ = limit; - return *this; - } - - // Set the callback manager for observability - RunnableConfig& withCallbacks( - std::shared_ptr callbacks) { - callbacks_ = std::move(callbacks); - return *this; - } - - // Accessors - const std::map& tags() const { return tags_; } - - const std::map& metadata() const { return metadata_; } - - optional tag(const std::string& key) const { - auto it = tags_.find(key); - if (it != tags_.end()) { - return mcp::make_optional(it->second); - } - return nullopt; - } - - const std::string& runName() const { return run_name_; } - - size_t maxConcurrency() const { return max_concurrency_; } - - std::chrono::milliseconds timeout() const { return timeout_ms_; } - - size_t recursionLimit() const { return recursion_limit_; } - - // Get the callback manager (may be null) - std::shared_ptr callbacks() const { - return callbacks_; - } - - // Check if callbacks are configured - bool hasCallbacks() const { return callbacks_ != nullptr; } - - // Merge another config into this one (other takes precedence) - RunnableConfig& merge(const RunnableConfig& other) { - for (const auto& kv : other.tags_) { - tags_[kv.first] = kv.second; - } - for (const auto& kv : other.metadata_) { - metadata_[kv.first] = kv.second; - } - if (!other.run_name_.empty()) { - run_name_ = other.run_name_; - } - if (other.max_concurrency_ > 0) { - max_concurrency_ = other.max_concurrency_; - } - if (other.timeout_ms_.count() > 0) { - timeout_ms_ = other.timeout_ms_; - } - if (other.recursion_limit_ > 0) { - recursion_limit_ = other.recursion_limit_; - } - if (other.callbacks_) { - callbacks_ = other.callbacks_; - } - return *this; - } - - // Create a child config that inherits from this config - RunnableConfig child() const { - RunnableConfig child_config = *this; - // Decrement recursion limit for child - if (child_config.recursion_limit_ > 0) { - child_config.recursion_limit_--; - } - return child_config; - } - - private: - std::map tags_; - std::map metadata_; - std::string run_name_; - size_t max_concurrency_ = 0; // 0 means unlimited - std::chrono::milliseconds timeout_ms_{0}; // 0 means no timeout - size_t recursion_limit_ = 25; // Default recursion limit - std::shared_ptr callbacks_; // Observability hooks -}; - -} // namespace core -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/core/lambda.h b/third_party/gopher-orch/include/gopher/orch/core/lambda.h deleted file mode 100644 index 5cda34c2..00000000 --- a/third_party/gopher-orch/include/gopher/orch/core/lambda.h +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once - -// Lambda - Create Runnable from a function or lambda -// Enables quick creation of custom operations without defining new classes - -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace core { - -// Synchronous function signature: (Input, Config) -> Result -// Use this when the operation can complete immediately -template -using SyncFunc = - std::function(const Input&, const RunnableConfig&)>; - -// Asynchronous function signature: (Input, Config, Dispatcher&, Callback) -// Use this when the operation needs async I/O or timer-based delays -template -using AsyncFunc = std::function)>; - -// Lambda Runnable - wraps a function as a Runnable -// -// Supports both synchronous and asynchronous functions: -// - Sync functions are posted to dispatcher for execution -// - Async functions are called directly (they manage their own posting) -template -class Lambda : public Runnable { - public: - using Callback = typename Runnable::Callback; - - // Create from synchronous function - // The function will be invoked via dispatcher.post() to ensure - // the callback runs in dispatcher context - static std::shared_ptr fromSync(SyncFunc func, - const std::string& name = "Lambda") { - return std::shared_ptr(new Lambda(std::move(func), name, true)); - } - - // Create from asynchronous function - // The function is responsible for calling the callback in dispatcher context - static std::shared_ptr fromAsync(AsyncFunc func, - const std::string& name = "Lambda") { - return std::shared_ptr(new Lambda(std::move(func), name, false)); - } - - std::string name() const override { return name_; } - - void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - if (is_sync_) { - // For sync functions, post to dispatcher to ensure callback runs in - // dispatcher context. Capture by value to ensure data survives - auto func = sync_func_; - dispatcher.post( - [func, input, config, callback = std::move(callback)]() mutable { - Result result = func(input, config); - callback(std::move(result)); - }); - } else { - // For async functions, call directly - they manage their own posting - async_func_(input, config, dispatcher, std::move(callback)); - } - } - - private: - // Private constructor - use factory methods - Lambda(SyncFunc func, std::string name, bool is_sync) - : sync_func_(std::move(func)), - name_(std::move(name)), - is_sync_(is_sync) {} - - Lambda(AsyncFunc func, std::string name, bool is_sync) - : async_func_(std::move(func)), - name_(std::move(name)), - is_sync_(is_sync) {} - - SyncFunc sync_func_; - AsyncFunc async_func_; - std::string name_; - bool is_sync_; -}; - -// Convenience factory functions - -// Create Lambda from sync function: (Input, Config) -> Result -template -std::shared_ptr> makeLambda( - SyncFunc func, const std::string& name = "Lambda") { - return Lambda::fromSync(std::move(func), name); -} - -// Create Lambda from simple sync function: Input -> Result -// (ignores config) -template -std::shared_ptr> makeLambda( - std::function(const Input&)> func, - const std::string& name = "Lambda") { - return Lambda::fromSync( - [func = std::move(func)](const Input& input, const RunnableConfig&) { - return func(input); - }, - name); -} - -// Create Lambda from async function -template -std::shared_ptr> makeLambdaAsync( - AsyncFunc func, const std::string& name = "Lambda") { - return Lambda::fromAsync(std::move(func), name); -} - -// JSON-specific Lambda (most common use case for FFI and dynamic composition) -using JsonLambda = Lambda; - -// Create JSON Lambda from sync function -inline std::shared_ptr makeJsonLambda( - SyncFunc func, - const std::string& name = "JsonLambda") { - return JsonLambda::fromSync(std::move(func), name); -} - -// Create JSON Lambda from simple sync function (ignores config) -inline std::shared_ptr makeJsonLambda( - std::function(const JsonValue&)> func, - const std::string& name = "JsonLambda") { - return JsonLambda::fromSync( - [func = std::move(func)](const JsonValue& input, const RunnableConfig&) { - return func(input); - }, - name); -} - -} // namespace core -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/core/runnable.h b/third_party/gopher-orch/include/gopher/orch/core/runnable.h deleted file mode 100644 index 8040caa4..00000000 --- a/third_party/gopher-orch/include/gopher/orch/core/runnable.h +++ /dev/null @@ -1,115 +0,0 @@ -#pragma once - -// Runnable - Universal composable interface -// Core abstraction for all operations in the orchestration framework -// -// Design principles: -// - Async-first: All operations use callbacks, no blocking -// - Dispatcher-native: Callbacks invoked in dispatcher thread context -// - Composable: Can be chained with pipe(), parallel(), etc. -// - Type-safe: Strong typing with explicit Input/Output types - -#include -#include - -#include "gopher/orch/core/config.h" -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { -namespace core { - -// Forward declarations for composition functions -template -class SequenceRunnable; - -template -class ParallelRunnable; - -// Runnable - Base class for all composable operations -// -// All callbacks are invoked in dispatcher thread context following the pattern: -// Create -> Configure -> Invoke (with dispatcher) -> Callback in dispatcher -// -// Implementations must: -// 1. Call callback exactly once (success or error) -// 2. Post callback to dispatcher if not already in dispatcher context -// 3. Handle cancellation gracefully -template -class Runnable : public std::enable_shared_from_this> { - public: - using InputType = Input; - using OutputType = Output; - using Callback = ResultCallback; - using Ptr = std::shared_ptr>; - - virtual ~Runnable() = default; - - // Human-readable name for debugging and tracing - virtual std::string name() const = 0; - - // Invoke the runnable asynchronously - // - input: The input value to process - // - config: Configuration options (tags, metadata, timeout, etc.) - // - dispatcher: Event loop for async operations - // - callback: Called exactly once with Result - // - // The callback MUST be invoked in the dispatcher's thread context. - // Implementations should post to dispatcher if running in a different thread. - virtual void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) = 0; - - // Convenience: invoke with default config - void invoke(const Input& input, Dispatcher& dispatcher, Callback callback) { - invoke(input, RunnableConfig(), dispatcher, std::move(callback)); - } - - // Get shared pointer to this runnable - Ptr shared() { return this->shared_from_this(); } - - protected: - Runnable() = default; - - // Helper to post callback to dispatcher - // Use this when the result is ready but we're not in dispatcher context - template - static void postResult(Dispatcher& dispatcher, - ResultCallback callback, - Result result) { - dispatcher.post( - [callback = std::move(callback), result = std::move(result)]() mutable { - callback(std::move(result)); - }); - } - - // Helper to post error to dispatcher - template - static void postError(Dispatcher& dispatcher, - ResultCallback callback, - int code, - const std::string& message) { - dispatcher.post([callback = std::move(callback), code, message]() { - callback(Result(Error(code, message))); - }); - } -}; - -// Type alias for JSON-to-JSON runnable (used for type-erased operations) -using JsonRunnable = Runnable; -using JsonRunnablePtr = std::shared_ptr; - -// Concept-like trait to check if a type is a Runnable -template -struct is_runnable : std::false_type {}; - -template -struct is_runnable> : std::true_type {}; - -template -struct is_runnable>> : std::true_type {}; - -} // namespace core -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/core/types.h b/third_party/gopher-orch/include/gopher/orch/core/types.h deleted file mode 100644 index 99f17bf1..00000000 --- a/third_party/gopher-orch/include/gopher/orch/core/types.h +++ /dev/null @@ -1,121 +0,0 @@ -#pragma once - -// Core types for gopher-orch framework -// Provides type aliases and common definitions used throughout the library - -#include -#include -#include // Required for placement new in variant -#include -#include - -// Use MCP core types - compat.h handles C++14/17 compatibility -#include "mcp/core/compat.h" -#include "mcp/core/result.h" -#include "mcp/core/type_helpers.h" -#include "mcp/event/libevent_dispatcher.h" -#include "mcp/json/json_bridge.h" -#include "mcp/types.h" - -namespace gopher { -namespace orch { -namespace core { - -// Re-export MCP types into our namespace for convenience -using mcp::Error; -using mcp::make_optional; -using mcp::nullopt; -using mcp::optional; -using mcp::Result; - -// JSON type alias - using MCP's JsonValue -using JsonValue = mcp::json::JsonValue; - -// Dispatcher type from MCP event system -using Dispatcher = mcp::event::Dispatcher; -using DispatcherPtr = std::unique_ptr; - -// Result callback type - invoked when async operation completes -// All callbacks are invoked in dispatcher thread context -template -using ResultCallback = std::function)>; - -// Void result for operations that don't return a value -using VoidResult = Result; -using VoidCallback = ResultCallback; - -// JSON-specific callback used for type-erased operations -using JsonCallback = ResultCallback; - -// Forward declarations -template -class Runnable; - -class RunnableConfig; - -// Type-erased runnable that works with JSON values -// This is the primary interface used by composition patterns and FFI -using JsonRunnable = Runnable; -using JsonRunnablePtr = std::shared_ptr; - -// Error codes specific to orchestration -// Using enum for C++14 compatibility (constexpr static members need out-of-line -// definition) -namespace OrchError { -enum : int { - OK = 0, - INVALID_ARGUMENT = -1, - TOOL_NOT_FOUND = -2, - CONNECTION_FAILED = -3, - TIMEOUT = -4, - CANCELLED = -5, - GUARD_REJECTED = -6, - INVALID_TRANSITION = -7, - APPROVAL_DENIED = -8, - CIRCUIT_OPEN = -9, - FALLBACK_EXHAUSTED = -10, - NOT_CONNECTED = -11, - INTERNAL_ERROR = -99 -}; -} // namespace OrchError - -// Helper to create error results -template -inline Result makeOrchError(int code, const std::string& message) { - return Result(Error(code, message)); -} - -// Helper to create success results -// Uses decay to remove const/reference qualifiers for proper Result type -template -inline Result::type> makeSuccess(T&& value) { - return Result::type>(std::forward(value)); -} - -// Helper to check if result is successful -template -inline bool isSuccess(const Result& result) { - return mcp::holds_alternative(result); -} - -// Helper to check if result is an error -template -inline bool isError(const Result& result) { - return mcp::holds_alternative(result); -} - -// Helper to get value from result (undefined behavior if error) -template -inline const T& getValue(const Result& result) { - return mcp::get(result); -} - -// Helper to get error from result (undefined behavior if success) -template -inline const Error& getError(const Result& result) { - return mcp::get(result); -} - -} // namespace core -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi.h b/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi.h deleted file mode 100644 index ca6b9e4d..00000000 --- a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi.h +++ /dev/null @@ -1,1406 +0,0 @@ -/** - * @file orch_ffi.h - * @brief FFI-friendly C API for gopher-orch orchestration framework - * - * This header provides the complete C API for the gopher-orch C++ framework. - * It follows an event-driven, dispatcher thread-confined architecture while - * ensuring FFI-safety and automatic resource management through RAII. - * - * Architecture: - * - All operations happen in dispatcher thread context - * - Callbacks are invoked in dispatcher thread - * - RAII guards ensure automatic cleanup - * - FFI-safe types for cross-language bindings - * - Follows Create -> Configure -> Use -> Destroy lifecycle - * - * Memory Management: - * - All handles are reference-counted internally - * - Automatic cleanup through RAII guards - * - Optional manual resource management for FFI - * - Thread-safe resource tracking in debug mode - * - * Key Design Decision - JSON-to-JSON FFI Boundary: - * - All Runnable templates are type-erased to JSON->JSON - * - This provides the cleanest FFI surface (80% of use cases) - * - Target languages handle typing in their wrapper layers - * - For custom types, use JSON serialization at the boundary - * - * Usage from other languages: - * - Python: ctypes/cffi wrapper, or pybind11 for direct C++ binding - * - Node.js: nbind or N-API native addon - * - Rust: cxx crate or bindgen for C API - * - Go: cgo with C API - * - Ruby: Rice gem (pybind11-like) or FFI gem - * - Lua: sol2 or LuaBridge - */ - -#ifndef GOPHER_ORCH_FFI_H -#define GOPHER_ORCH_FFI_H - -#include "orch_ffi_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* ============================================================================ - * Version and Initialization - * ============================================================================ - */ - -#define GOPHER_ORCH_VERSION_MAJOR 1 -#define GOPHER_ORCH_VERSION_MINOR 0 -#define GOPHER_ORCH_VERSION_PATCH 0 - -/** - * Get runtime version (for ABI compatibility check) - * Caller should verify version matches compiled headers - */ -GOPHER_ORCH_API void gopher_orch_version(int* major, - int* minor, - int* patch) GOPHER_ORCH_NOEXCEPT; - -/** - * Get version as string - * @return Version string (e.g., "1.0.0"), do not free - */ -GOPHER_ORCH_API const char* gopher_orch_version_string(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Initialize library (call once at startup) - * Must be called before any other API functions - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_init(void) GOPHER_ORCH_NOEXCEPT; - -/** - * Shutdown library (call once at shutdown) - * Cleans up all resources and checks for leaks - */ -GOPHER_ORCH_API void gopher_orch_shutdown(void) GOPHER_ORCH_NOEXCEPT; - -/** - * Check if library is initialized - * @return GOPHER_ORCH_TRUE if initialized - */ -GOPHER_ORCH_API gopher_orch_bool_t gopher_orch_is_initialized(void) - GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Error Handling - * ============================================================================ - */ - -/** - * Get last error info for current thread - * @return Error info struct, or NULL if no error - */ -GOPHER_ORCH_API const gopher_orch_error_info_t* gopher_orch_last_error(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Get human-readable error name - * @param code Error code - * @return Error name string (e.g., "GOPHER_ORCH_ERROR_TIMEOUT"), do not free - */ -GOPHER_ORCH_API const char* gopher_orch_error_name(gopher_orch_error_t code) - GOPHER_ORCH_NOEXCEPT; - -/** - * Clear last error for current thread - */ -GOPHER_ORCH_API void gopher_orch_clear_error(void) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Memory Management - * ============================================================================ - */ - -/** - * Free memory allocated by the library - * Use for strings returned with OWNED semantics - * @param ptr Pointer to free (NULL-safe) - */ -GOPHER_ORCH_API void gopher_orch_free(void* ptr) GOPHER_ORCH_NOEXCEPT; - -/** - * Free string buffer - * @param buffer String buffer to free (NULL-safe) - */ -GOPHER_ORCH_API void gopher_orch_string_buffer_free( - gopher_orch_string_buffer_t* buffer) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * RAII Guard Functions - * - * Guards provide automatic cleanup when resources go out of scope. - * This pattern works well with FFI - caller creates guard, performs - * operations, then either commits (takes ownership) or lets guard cleanup. - * ============================================================================ - */ - -/** - * Create a RAII guard for a handle with automatic cleanup - * @param handle Handle to guard (takes ownership) - * @param type Type of handle for validation - * @return Guard handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_guard_t gopher_orch_guard_create( - void* handle, gopher_orch_type_id_t type) GOPHER_ORCH_NOEXCEPT; - -/** - * Create a RAII guard with custom cleanup function - * @param handle Handle to guard (takes ownership) - * @param type Type of handle for validation - * @param cleanup Custom cleanup function - * @return Guard handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_guard_t gopher_orch_guard_create_custom( - void* handle, - gopher_orch_type_id_t type, - gopher_orch_cleanup_fn cleanup) GOPHER_ORCH_NOEXCEPT; - -/** - * Release resource from guard (prevents automatic cleanup) - * @param guard Guard handle (will be nullified) - * @return Original handle (caller takes ownership) - */ -GOPHER_ORCH_API void* gopher_orch_guard_release(gopher_orch_guard_t* guard) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy guard and cleanup resource - * @param guard Guard handle (will be nullified) - */ -GOPHER_ORCH_API void gopher_orch_guard_destroy(gopher_orch_guard_t* guard) - GOPHER_ORCH_NOEXCEPT; - -/** - * Check if guard is valid and holds a resource - * @param guard Guard handle - * @return GOPHER_ORCH_TRUE if valid - */ -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_guard_is_valid(gopher_orch_guard_t guard) GOPHER_ORCH_NOEXCEPT; - -/** - * Get the guarded resource without releasing ownership - * @param guard Guard handle - * @return Guarded resource or NULL - */ -GOPHER_ORCH_API void* gopher_orch_guard_get(gopher_orch_guard_t guard) - GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Transaction Management - * - * Transactions ensure all-or-nothing semantics for multi-resource operations. - * Use when creating multiple resources that depend on each other. - * ============================================================================ - */ - -/** - * Create a new transaction with default options - * @return Transaction handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_transaction_t gopher_orch_transaction_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Create a new transaction with custom options - * @param opts Transaction options (may be NULL for defaults) - * @return Transaction handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_transaction_t gopher_orch_transaction_create_ex( - const gopher_orch_transaction_opts_t* opts) GOPHER_ORCH_NOEXCEPT; - -/** - * Add resource to transaction with automatic cleanup - * @param txn Transaction handle - * @param handle Resource handle (ownership transferred) - * @param type Resource type for validation - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_transaction_add(gopher_orch_transaction_t txn, - void* handle, - gopher_orch_type_id_t type) GOPHER_ORCH_NOEXCEPT; - -/** - * Commit transaction (release resources, prevent cleanup) - * @param txn Transaction handle (will be nullified) - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_transaction_commit( - gopher_orch_transaction_t* txn) GOPHER_ORCH_NOEXCEPT; - -/** - * Rollback transaction (cleanup all resources) - * @param txn Transaction handle (will be nullified) - */ -GOPHER_ORCH_API void gopher_orch_transaction_rollback( - gopher_orch_transaction_t* txn) GOPHER_ORCH_NOEXCEPT; - -/** - * Get number of resources in transaction - * @param txn Transaction handle - * @return Number of resources - */ -GOPHER_ORCH_API gopher_orch_size_t gopher_orch_transaction_size( - gopher_orch_transaction_t txn) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Cancellation Token - * - * Tokens allow cancelling async operations from any thread. - * ============================================================================ - */ - -/** - * Create cancellation token - * @return Token handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_cancel_token_t gopher_orch_cancel_token_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy cancellation token - * @param token Token handle - */ -GOPHER_ORCH_API void gopher_orch_cancel_token_destroy( - gopher_orch_cancel_token_t token) GOPHER_ORCH_NOEXCEPT; - -/** - * Request cancellation - safe to call from any thread - * @param token Token handle - */ -GOPHER_ORCH_API void gopher_orch_cancel_token_cancel( - gopher_orch_cancel_token_t token) GOPHER_ORCH_NOEXCEPT; - -/** - * Check if cancelled - * @param token Token handle - * @return GOPHER_ORCH_TRUE if cancelled - */ -GOPHER_ORCH_API gopher_orch_bool_t gopher_orch_cancel_token_is_cancelled( - gopher_orch_cancel_token_t token) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Dispatcher (Event Loop) - * - * The dispatcher provides an event loop for async operations. - * All callbacks are invoked in the dispatcher thread context. - * ============================================================================ - */ - -/** - * Create dispatcher - * @return Dispatcher handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_dispatcher_t gopher_orch_dispatcher_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Create dispatcher with RAII guard - * @param guard Output: RAII guard for automatic cleanup - * @return Dispatcher handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_dispatcher_t gopher_orch_dispatcher_create_guarded( - gopher_orch_guard_t* guard) GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy dispatcher - * @param dispatcher Dispatcher handle - */ -GOPHER_ORCH_API void gopher_orch_dispatcher_destroy( - gopher_orch_dispatcher_t dispatcher) GOPHER_ORCH_NOEXCEPT; - -/** - * Run dispatcher (blocks until stopped) - * @param dispatcher Dispatcher handle - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_dispatcher_run( - gopher_orch_dispatcher_t dispatcher) GOPHER_ORCH_NOEXCEPT; - -/** - * Run dispatcher for one iteration - * @param dispatcher Dispatcher handle - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_dispatcher_run_one( - gopher_orch_dispatcher_t dispatcher) GOPHER_ORCH_NOEXCEPT; - -/** - * Run dispatcher for specified duration - * @param dispatcher Dispatcher handle - * @param timeout_ms Maximum time in milliseconds - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_dispatcher_run_timeout(gopher_orch_dispatcher_t dispatcher, - uint64_t timeout_ms) GOPHER_ORCH_NOEXCEPT; - -/** - * Stop dispatcher - * @param dispatcher Dispatcher handle - */ -GOPHER_ORCH_API void gopher_orch_dispatcher_stop( - gopher_orch_dispatcher_t dispatcher) GOPHER_ORCH_NOEXCEPT; - -/** - * Post work to dispatcher thread - * @param dispatcher Dispatcher handle - * @param work Work function to execute - * @param user_context User context passed to work function - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_dispatcher_post(gopher_orch_dispatcher_t dispatcher, - gopher_orch_work_fn work, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Check if current thread is dispatcher thread - * @param dispatcher Dispatcher handle - * @return GOPHER_ORCH_TRUE if in dispatcher thread - */ -GOPHER_ORCH_API gopher_orch_bool_t gopher_orch_dispatcher_is_thread( - gopher_orch_dispatcher_t dispatcher) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * JSON Value API - * - * JSON is the primary data type at the FFI boundary. - * All complex data is passed as JSON values. - * ============================================================================ - */ - -/* Creation */ -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_null(void) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_json_t -gopher_orch_json_bool(gopher_orch_bool_t value) GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_int(int64_t value) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_double(double value) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_string(const char* value) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_object(void) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_array(void) - GOPHER_ORCH_NOEXCEPT; - -/* Lifecycle - reference counting */ -GOPHER_ORCH_API void gopher_orch_json_add_ref(gopher_orch_json_t handle) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API void gopher_orch_json_release(gopher_orch_json_t handle) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_json_t -gopher_orch_json_clone(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; - -/* Object operations */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_json_set( - gopher_orch_json_t obj, const char* key, gopher_orch_json_t value) - GOPHER_ORCH_NOEXCEPT; /* Takes ownership of value */ - -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_get(gopher_orch_json_t obj, - const char* key) - GOPHER_ORCH_NOEXCEPT; /* Returns BORROWED reference */ - -GOPHER_ORCH_API gopher_orch_bool_t gopher_orch_json_has( - gopher_orch_json_t obj, const char* key) GOPHER_ORCH_NOEXCEPT; - -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_json_remove( - gopher_orch_json_t obj, const char* key) GOPHER_ORCH_NOEXCEPT; - -/* Array operations */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_json_push(gopher_orch_json_t arr, gopher_orch_json_t value) - GOPHER_ORCH_NOEXCEPT; /* Takes ownership of value */ - -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_at(gopher_orch_json_t arr, - gopher_orch_size_t index) - GOPHER_ORCH_NOEXCEPT; /* Returns BORROWED reference */ - -GOPHER_ORCH_API gopher_orch_size_t -gopher_orch_json_length(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; - -/* Type checking */ -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_json_is_null(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_json_is_bool(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_json_is_number(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_json_is_string(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_json_is_object(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_json_is_array(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; - -/* Value extraction */ -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_json_as_bool(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API int64_t gopher_orch_json_as_int(gopher_orch_json_t handle) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API double gopher_orch_json_as_double(gopher_orch_json_t handle) - GOPHER_ORCH_NOEXCEPT; -GOPHER_ORCH_API const char* gopher_orch_json_as_string( - gopher_orch_json_t handle) - GOPHER_ORCH_NOEXCEPT; /* Returns BORROWED string */ - -/* Serialization */ -GOPHER_ORCH_API char* gopher_orch_json_stringify(gopher_orch_json_t handle) - GOPHER_ORCH_NOEXCEPT; /* OWNED: Caller must gopher_orch_free() */ - -GOPHER_ORCH_API char* gopher_orch_json_stringify_pretty( - gopher_orch_json_t handle) - GOPHER_ORCH_NOEXCEPT; /* OWNED: Caller must gopher_orch_free() */ - -GOPHER_ORCH_API gopher_orch_json_t gopher_orch_json_parse(const char* json_str) - GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * JSON Iterator API - * - * Iterate over object keys and array elements. - * ============================================================================ - */ - -/** - * Create iterator for JSON object or array - * @param handle JSON object or array handle - * @return Iterator handle or NULL - */ -GOPHER_ORCH_API gopher_orch_iterator_t -gopher_orch_json_iter(gopher_orch_json_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy iterator - * @param iter Iterator handle - */ -GOPHER_ORCH_API void gopher_orch_iter_destroy(gopher_orch_iterator_t iter) - GOPHER_ORCH_NOEXCEPT; - -/** - * Advance to next element - * @param iter Iterator handle - * @return GOPHER_ORCH_TRUE if advanced, GOPHER_ORCH_FALSE if exhausted - */ -GOPHER_ORCH_API gopher_orch_bool_t -gopher_orch_iter_next(gopher_orch_iterator_t iter) GOPHER_ORCH_NOEXCEPT; - -/** - * Get current key (for object iterators) - * @param iter Iterator handle - * @return Key string, BORROWED - valid until next iter_next or iter_destroy - */ -GOPHER_ORCH_API const char* gopher_orch_iter_key(gopher_orch_iterator_t iter) - GOPHER_ORCH_NOEXCEPT; - -/** - * Get current value - * @param iter Iterator handle - * @return Value handle, BORROWED - valid until next iter_next or iter_destroy - */ -GOPHER_ORCH_API gopher_orch_json_t -gopher_orch_iter_value(gopher_orch_iterator_t iter) GOPHER_ORCH_NOEXCEPT; - -/** - * Get current array index (for array iterators) - * @param iter Iterator handle - * @return Current index - */ -GOPHER_ORCH_API gopher_orch_size_t -gopher_orch_iter_index(gopher_orch_iterator_t iter) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Runnable API (Type-erased JSON-to-JSON) - * - * Core abstraction: all operations are exposed as JSON->JSON transformations. - * This provides the cleanest FFI surface. - * ============================================================================ - */ - -/** - * Increment reference count - * @param handle Runnable handle - */ -GOPHER_ORCH_API void gopher_orch_runnable_add_ref(gopher_orch_runnable_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Decrement reference count (destroys when count reaches 0) - * @param handle Runnable handle - */ -GOPHER_ORCH_API void gopher_orch_runnable_release(gopher_orch_runnable_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Get runnable name - * @param handle Runnable handle - * @return Name string, BORROWED - */ -GOPHER_ORCH_API const char* gopher_orch_runnable_name( - gopher_orch_runnable_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Invoke runnable asynchronously - * - * @param handle Runnable handle - * @param input Input JSON value - * @param config Configuration handle (NULL for defaults) - * @param dispatcher Dispatcher handle - * @param cancel_token Cancellation token (NULL if not needed) - * @param callback Completion callback - * @param user_context User context for callback - */ -GOPHER_ORCH_API void gopher_orch_runnable_invoke( - gopher_orch_runnable_t handle, - gopher_orch_json_t input, - gopher_orch_config_t config, - gopher_orch_dispatcher_t dispatcher, - gopher_orch_cancel_token_t cancel_token, - gopher_orch_completion_fn callback, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Invoke runnable synchronously (blocks until complete) - * - * @param handle Runnable handle - * @param input Input JSON value - * @param config Configuration handle (NULL for defaults) - * @param dispatcher Dispatcher handle - * @param cancel_token Cancellation token (NULL if not needed) - * @param out_result Output: result JSON handle (OWNED) - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_runnable_invoke_sync( - gopher_orch_runnable_t handle, - gopher_orch_json_t input, - gopher_orch_config_t config, - gopher_orch_dispatcher_t dispatcher, - gopher_orch_cancel_token_t cancel_token, - gopher_orch_json_t* out_result) GOPHER_ORCH_NOEXCEPT; - -/** - * Create lambda runnable from C function - * This is the primary way FFI users create custom runnables. - * - * @param fn Lambda function - * @param user_context User context passed to fn - * @param name Runnable name - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t -gopher_orch_lambda_create(gopher_orch_lambda_fn fn, - void* user_context, - const char* name) GOPHER_ORCH_NOEXCEPT; - -/** - * Create lambda with destructor for context cleanup - * - * @param fn Lambda function - * @param user_context User context passed to fn - * @param destructor Called when runnable is destroyed to cleanup context - * @param name Runnable name - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t -gopher_orch_lambda_create_with_destructor(gopher_orch_lambda_fn fn, - void* user_context, - gopher_orch_destructor_fn destructor, - const char* name) - GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Configuration API - * ============================================================================ - */ - -/** - * Create default configuration - * @return Config handle or NULL - */ -GOPHER_ORCH_API gopher_orch_config_t gopher_orch_config_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy configuration - * @param config Config handle - */ -GOPHER_ORCH_API void gopher_orch_config_destroy(gopher_orch_config_t config) - GOPHER_ORCH_NOEXCEPT; - -/** - * Set callback manager - * @param config Config handle - * @param manager Callback manager handle - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_config_set_callbacks( - gopher_orch_config_t config, - gopher_orch_callback_manager_t manager) GOPHER_ORCH_NOEXCEPT; - -/** - * Add tag to configuration - * @param config Config handle - * @param tag Tag string - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_config_add_tag( - gopher_orch_config_t config, const char* tag) GOPHER_ORCH_NOEXCEPT; - -/** - * Set metadata value - * @param config Config handle - * @param key Metadata key - * @param value Metadata value (takes ownership) - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_config_set_metadata(gopher_orch_config_t config, - const char* key, - gopher_orch_json_t value) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Composition API - Sequence - * - * Sequences execute runnables in order, passing output to next input. - * Builder pattern: create -> add steps -> build - * ============================================================================ - */ - -/** - * Create sequence builder - * @return Sequence builder handle or NULL - */ -GOPHER_ORCH_API gopher_orch_sequence_t gopher_orch_sequence_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy sequence builder (safe to call after build) - * @param handle Sequence builder handle - */ -GOPHER_ORCH_API void gopher_orch_sequence_destroy(gopher_orch_sequence_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Add step to sequence - * @param handle Sequence builder handle - * @param step Runnable to add (reference count incremented) - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_sequence_add(gopher_orch_sequence_t handle, - gopher_orch_runnable_t step) GOPHER_ORCH_NOEXCEPT; - -/** - * Build sequence into runnable - * @param handle Sequence builder handle - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t -gopher_orch_sequence_build(gopher_orch_sequence_t handle) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Composition API - Parallel - * - * Parallel executes multiple runnables concurrently, collecting results. - * ============================================================================ - */ - -/** - * Create parallel builder - * @return Parallel builder handle or NULL - */ -GOPHER_ORCH_API gopher_orch_parallel_t gopher_orch_parallel_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy parallel builder - * @param handle Parallel builder handle - */ -GOPHER_ORCH_API void gopher_orch_parallel_destroy(gopher_orch_parallel_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Add branch to parallel - * @param handle Parallel builder handle - * @param key Result key - * @param runnable Runnable for this branch - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_parallel_add(gopher_orch_parallel_t handle, - const char* key, - gopher_orch_runnable_t runnable) GOPHER_ORCH_NOEXCEPT; - -/** - * Build parallel into runnable - * @param handle Parallel builder handle - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t -gopher_orch_parallel_build(gopher_orch_parallel_t handle) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Composition API - Router - * - * Router selects between runnables based on conditions. - * ============================================================================ - */ - -/** - * Create router builder - * @return Router builder handle or NULL - */ -GOPHER_ORCH_API gopher_orch_router_t gopher_orch_router_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy router builder - * @param handle Router builder handle - */ -GOPHER_ORCH_API void gopher_orch_router_destroy(gopher_orch_router_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Add conditional route - * @param handle Router builder handle - * @param condition Condition function - * @param user_context Context for condition function - * @param runnable Runnable to use if condition matches - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_router_when(gopher_orch_router_t handle, - gopher_orch_condition_fn condition, - void* user_context, - gopher_orch_runnable_t runnable) GOPHER_ORCH_NOEXCEPT; - -/** - * Set default route - * @param handle Router builder handle - * @param runnable Runnable to use when no conditions match - * @return GOPHER_ORCH_OK on success - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_router_otherwise( - gopher_orch_router_t handle, - gopher_orch_runnable_t runnable) GOPHER_ORCH_NOEXCEPT; - -/** - * Build router into runnable - * @param handle Router builder handle - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t -gopher_orch_router_build(gopher_orch_router_t handle) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Resilience API - * - * Wrappers that add resilience patterns to runnables. - * ============================================================================ - */ - -/** - * Create retry wrapper - * @param inner Inner runnable (reference count incremented) - * @param policy Retry policy - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t gopher_orch_retry_create( - gopher_orch_runnable_t inner, - const gopher_orch_retry_policy_t* policy) GOPHER_ORCH_NOEXCEPT; - -/** - * Create timeout wrapper - * @param inner Inner runnable - * @param timeout_ms Timeout in milliseconds - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t gopher_orch_timeout_create( - gopher_orch_runnable_t inner, uint64_t timeout_ms) GOPHER_ORCH_NOEXCEPT; - -/** - * Create fallback wrapper - * @param primary Primary runnable - * @param fallbacks Array of fallback runnables - * @param fallback_count Number of fallbacks - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t gopher_orch_fallback_create( - gopher_orch_runnable_t primary, - gopher_orch_runnable_t* fallbacks, - gopher_orch_size_t fallback_count) GOPHER_ORCH_NOEXCEPT; - -/** - * Create circuit breaker wrapper - * @param inner Inner runnable - * @param policy Circuit breaker policy - * @return Runnable handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_runnable_t gopher_orch_circuit_breaker_create( - gopher_orch_runnable_t inner, - const gopher_orch_circuit_breaker_policy_t* policy) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Server API - * - * MCP server connections for tool invocation. - * ============================================================================ - */ - -/** - * Increment server reference count - */ -GOPHER_ORCH_API void gopher_orch_server_add_ref(gopher_orch_server_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Decrement server reference count - */ -GOPHER_ORCH_API void gopher_orch_server_release(gopher_orch_server_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Get server ID - */ -GOPHER_ORCH_API const char* gopher_orch_server_id(gopher_orch_server_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Get server name - */ -GOPHER_ORCH_API const char* gopher_orch_server_name(gopher_orch_server_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Check if server is connected - */ -GOPHER_ORCH_API gopher_orch_bool_t gopher_orch_server_is_connected( - gopher_orch_server_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Get tool count - */ -GOPHER_ORCH_API gopher_orch_size_t -gopher_orch_server_tool_count(gopher_orch_server_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Get tool name by index - */ -GOPHER_ORCH_API const char* gopher_orch_server_tool_name( - gopher_orch_server_t handle, gopher_orch_size_t index) GOPHER_ORCH_NOEXCEPT; - -/** - * Get tool as runnable - * @param handle Server handle - * @param tool_name Tool name - * @return Runnable handle or NULL if not found - */ -GOPHER_ORCH_API gopher_orch_runnable_t gopher_orch_server_tool( - gopher_orch_server_t handle, const char* tool_name) GOPHER_ORCH_NOEXCEPT; - -/** - * Call tool directly (async) - */ -GOPHER_ORCH_API void gopher_orch_server_call_tool( - gopher_orch_server_t handle, - const char* tool_name, - gopher_orch_json_t arguments, - gopher_orch_dispatcher_t dispatcher, - gopher_orch_cancel_token_t cancel_token, - gopher_orch_completion_fn callback, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Mock Server API (for testing) - * ============================================================================ - */ - -/** - * Create mock server - */ -GOPHER_ORCH_API gopher_orch_server_t -gopher_orch_mock_server_create(const char* name) GOPHER_ORCH_NOEXCEPT; - -/** - * Add tool to mock server - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_mock_server_add_tool(gopher_orch_server_t handle, - const char* tool_name, - const char* description) GOPHER_ORCH_NOEXCEPT; - -/** - * Set tool response - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_mock_server_set_response( - gopher_orch_server_t handle, - const char* tool_name, - gopher_orch_json_t response) GOPHER_ORCH_NOEXCEPT; - -/** - * Set tool error - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_mock_server_set_error( - gopher_orch_server_t handle, - const char* tool_name, - gopher_orch_error_t error_code, - const char* error_message) GOPHER_ORCH_NOEXCEPT; - -/** - * Get call count - */ -GOPHER_ORCH_API gopher_orch_size_t gopher_orch_mock_server_call_count( - gopher_orch_server_t handle, const char* tool_name) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * MCP Server API (real connections) - * ============================================================================ - */ - -/** - * Server creation callback - */ -typedef void (*gopher_orch_server_fn)(void* user_context, - gopher_orch_error_t error, - gopher_orch_server_t server); - -/** - * Create MCP server connection (async) - */ -GOPHER_ORCH_API void gopher_orch_mcp_server_create( - const gopher_orch_mcp_config_t* config, - gopher_orch_dispatcher_t dispatcher, - gopher_orch_server_fn callback, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Close MCP server connection - */ -GOPHER_ORCH_API void gopher_orch_mcp_server_close(gopher_orch_server_t handle, - gopher_orch_work_fn on_closed, - void* user_context) - GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Callback Manager API (Observability) - * ============================================================================ - */ - -/** - * Create callback manager - */ -GOPHER_ORCH_API gopher_orch_callback_manager_t -gopher_orch_callback_manager_create(void) GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy callback manager - */ -GOPHER_ORCH_API void gopher_orch_callback_manager_destroy( - gopher_orch_callback_manager_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Add callback handler - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_callback_manager_add_handler( - gopher_orch_callback_manager_t handle, - const gopher_orch_callback_handler_config_t* config) GOPHER_ORCH_NOEXCEPT; - -/** - * Get handler count - */ -GOPHER_ORCH_API gopher_orch_size_t gopher_orch_callback_manager_handler_count( - gopher_orch_callback_manager_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Clear all handlers - */ -GOPHER_ORCH_API void gopher_orch_callback_manager_clear( - gopher_orch_callback_manager_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Create child manager (inherits handlers, sets parent_run_id) - */ -GOPHER_ORCH_API gopher_orch_callback_manager_t -gopher_orch_callback_manager_child(gopher_orch_callback_manager_t handle) - GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Approval Handler API (Human-in-the-Loop) - * ============================================================================ - */ - -/** - * Create auto-approve handler (for testing) - */ -GOPHER_ORCH_API gopher_orch_approval_handler_t -gopher_orch_auto_approval_create(const char* reason) GOPHER_ORCH_NOEXCEPT; - -/** - * Create auto-deny handler (for testing) - */ -GOPHER_ORCH_API gopher_orch_approval_handler_t -gopher_orch_auto_deny_create(const char* reason) GOPHER_ORCH_NOEXCEPT; - -/** - * Create callback-based approval handler - */ -GOPHER_ORCH_API gopher_orch_approval_handler_t -gopher_orch_callback_approval_create(gopher_orch_approval_fn fn, - void* user_context, - gopher_orch_destructor_fn destructor) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy approval handler - */ -GOPHER_ORCH_API void gopher_orch_approval_handler_destroy( - gopher_orch_approval_handler_t handle) GOPHER_ORCH_NOEXCEPT; - -/** - * Create human approval wrapper - */ -GOPHER_ORCH_API gopher_orch_runnable_t -gopher_orch_human_approval_create(gopher_orch_runnable_t inner, - gopher_orch_approval_handler_t handler, - const char* prompt) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * State Machine API (FSM with int32_t states/events) - * ============================================================================ - */ - -/** - * Create state machine - */ -GOPHER_ORCH_API gopher_orch_fsm_t gopher_orch_fsm_create(int32_t initial_state) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy state machine - */ -GOPHER_ORCH_API void gopher_orch_fsm_destroy(gopher_orch_fsm_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Add transition - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_fsm_add_transition(gopher_orch_fsm_t handle, - int32_t from_state, - int32_t event, - int32_t to_state) GOPHER_ORCH_NOEXCEPT; - -/** - * Set guard for transition - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_fsm_set_guard(gopher_orch_fsm_t handle, - int32_t from_state, - int32_t event, - gopher_orch_guard_fn guard, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Set action for transition - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_fsm_set_action(gopher_orch_fsm_t handle, - int32_t from_state, - int32_t event, - gopher_orch_action_fn action, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Set state entry action - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_fsm_on_enter(gopher_orch_fsm_t handle, - int32_t state, - gopher_orch_action_fn action, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Set state exit action - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_fsm_on_exit(gopher_orch_fsm_t handle, - int32_t state, - gopher_orch_action_fn action, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Set transition observer - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_fsm_set_observer(gopher_orch_fsm_t handle, - gopher_orch_transition_fn observer, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Get current state - */ -GOPHER_ORCH_API int32_t gopher_orch_fsm_current_state(gopher_orch_fsm_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Check if event can trigger transition - */ -GOPHER_ORCH_API gopher_orch_bool_t gopher_orch_fsm_can_trigger( - gopher_orch_fsm_t handle, int32_t event) GOPHER_ORCH_NOEXCEPT; - -/** - * Trigger event (sync) - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_fsm_trigger(gopher_orch_fsm_t handle, - int32_t event, - int32_t* out_new_state) GOPHER_ORCH_NOEXCEPT; - -/** - * Trigger event (async) - */ -typedef void (*gopher_orch_fsm_trigger_fn)(void* user_context, - gopher_orch_error_t error, - int32_t new_state); - -GOPHER_ORCH_API void gopher_orch_fsm_trigger_async( - gopher_orch_fsm_t handle, - int32_t event, - gopher_orch_dispatcher_t dispatcher, - gopher_orch_fsm_trigger_fn callback, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * State Graph API - * - * Graph-based workflows with conditional edges (JSON state). - * ============================================================================ - */ - -/** - * Create state graph builder - */ -GOPHER_ORCH_API gopher_orch_graph_t gopher_orch_graph_create(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Destroy state graph builder - */ -GOPHER_ORCH_API void gopher_orch_graph_destroy(gopher_orch_graph_t handle) - GOPHER_ORCH_NOEXCEPT; - -/** - * Add node to graph - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_graph_add_node( - gopher_orch_graph_t handle, - const char* name, - gopher_orch_runnable_t runnable) GOPHER_ORCH_NOEXCEPT; - -/** - * Add edge from one node to another - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_graph_add_edge(gopher_orch_graph_t handle, - const char* from, - const char* to) GOPHER_ORCH_NOEXCEPT; - -/** - * Add conditional edge (router-style) - */ -GOPHER_ORCH_API gopher_orch_error_t -gopher_orch_graph_add_conditional_edge(gopher_orch_graph_t handle, - const char* from, - gopher_orch_edge_condition_fn condition, - void* user_context) GOPHER_ORCH_NOEXCEPT; - -/** - * Set entry point - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_graph_set_entry( - gopher_orch_graph_t handle, const char* node_name) GOPHER_ORCH_NOEXCEPT; - -/** - * Add state channel with reducer - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_graph_add_channel( - gopher_orch_graph_t handle, - const char* key, - gopher_orch_channel_type_t type) GOPHER_ORCH_NOEXCEPT; - -/** - * Compile graph into runnable - */ -GOPHER_ORCH_API gopher_orch_runnable_t -gopher_orch_graph_compile(gopher_orch_graph_t handle) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Resource Statistics and Debugging - * ============================================================================ - */ - -/** - * Get resource statistics - */ -GOPHER_ORCH_API gopher_orch_error_t gopher_orch_get_resource_stats( - gopher_orch_size_t* active_count, - gopher_orch_size_t* total_created, - gopher_orch_size_t* total_destroyed) GOPHER_ORCH_NOEXCEPT; - -/** - * Check for resource leaks - * @return Number of leaked resources - */ -GOPHER_ORCH_API gopher_orch_size_t gopher_orch_check_leaks(void) - GOPHER_ORCH_NOEXCEPT; - -/** - * Print leak report to stderr - */ -GOPHER_ORCH_API void gopher_orch_print_leak_report(void) GOPHER_ORCH_NOEXCEPT; - -/* ============================================================================ - * Agent API - * - * High-level AI agent functionality using ReActAgent pattern. - * ============================================================================ - */ - -/** - * Create agent from JSON server configuration - * @param provider_name LLM provider name (e.g., "AnthropicProvider") - * @param model_name Model name (e.g., "claude-3-haiku-20240307") - * @param server_json_config JSON string containing MCP server configuration - * @return Agent handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_agent_t gopher_orch_agent_create_by_json( - const char* provider_name, - const char* model_name, - const char* server_json_config) GOPHER_ORCH_NOEXCEPT; - -/** - * Create agent using API key (fetches server config automatically) - * @param provider_name LLM provider name (e.g., "AnthropicProvider") - * @param model_name Model name (e.g., "claude-3-haiku-20240307") - * @param api_key API key for fetching server configuration - * @return Agent handle or NULL on error - */ -GOPHER_ORCH_API gopher_orch_agent_t gopher_orch_agent_create_by_api_key( - const char* provider_name, - const char* model_name, - const char* api_key) GOPHER_ORCH_NOEXCEPT; - -/** - * Run agent query synchronously - * @param agent Agent handle - * @param query User query string - * @param timeout_ms Timeout in milliseconds (0 for no timeout) - * @return Response string (OWNED - caller must gopher_orch_free) - */ -GOPHER_ORCH_API char* gopher_orch_agent_run( - gopher_orch_agent_t agent, - const char* query, - uint64_t timeout_ms) GOPHER_ORCH_NOEXCEPT; - -/** - * Increment agent reference count - * @param agent Agent handle - */ -GOPHER_ORCH_API void gopher_orch_agent_add_ref(gopher_orch_agent_t agent) - GOPHER_ORCH_NOEXCEPT; - -/** - * Decrement agent reference count (destroys when count reaches 0) - * @param agent Agent handle - */ -GOPHER_ORCH_API void gopher_orch_agent_release(gopher_orch_agent_t agent) - GOPHER_ORCH_NOEXCEPT; - -/** - * Fetch MCP server configurations from API - * @param api_key API key for authentication - * @return JSON configuration string (OWNED - caller must gopher_orch_free) - */ -GOPHER_ORCH_API char* gopher_orch_api_fetch_servers(const char* api_key) - GOPHER_ORCH_NOEXCEPT; - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -/* ============================================================================ - * RAII Helper Macros (for C++ users of the C API) - * ============================================================================ - */ - -#ifdef __cplusplus - -#include -#include - -/* Automatic cleanup guard for any handle */ -#define GOPHER_ORCH_AUTO_GUARD(handle, type) \ - std::unique_ptr> _guard_##__LINE__( \ - handle, [](void* h) { \ - if (h) { \ - auto guard = gopher_orch_guard_create(h, type); \ - gopher_orch_guard_destroy(&guard); \ - } \ - }) - -/* Scoped transaction with automatic rollback */ -#define GOPHER_ORCH_SCOPED_TRANSACTION(name) \ - struct _TxnGuard_##__LINE__ { \ - gopher_orch_transaction_t txn; \ - bool committed = false; \ - _TxnGuard_##__LINE__() : txn(gopher_orch_transaction_create()) {} \ - ~_TxnGuard_##__LINE__() { \ - if (txn && !committed) { \ - gopher_orch_transaction_rollback(&txn); \ - } \ - } \ - void commit() { \ - if (txn) { \ - gopher_orch_transaction_commit(&txn); \ - committed = true; \ - } \ - } \ - } name - -#endif /* __cplusplus */ - -/* ============================================================================ - * RAII Patterns for C Users - * ============================================================================ - */ - -/* Guard creation macro */ -#define GOPHER_ORCH_GUARD_CREATE(handle, type) \ - gopher_orch_guard_create(handle, type) - -/* Safe resource release macro */ -#define GOPHER_ORCH_SAFE_RELEASE(guard_ptr) \ - do { \ - if (guard_ptr && *(guard_ptr)) { \ - gopher_orch_guard_destroy(guard_ptr); \ - } \ - } while (0) - -/* Safe transaction cleanup macro */ -#define GOPHER_ORCH_SAFE_TXN_CLEANUP(txn_ptr) \ - do { \ - if (txn_ptr && *(txn_ptr)) { \ - gopher_orch_transaction_rollback(txn_ptr); \ - } \ - } while (0) - -#endif /* GOPHER_ORCH_FFI_H */ diff --git a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_bridge.h b/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_bridge.h deleted file mode 100644 index d1dac078..00000000 --- a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_bridge.h +++ /dev/null @@ -1,865 +0,0 @@ -/** - * @file orch_ffi_bridge.h - * @brief Internal C++ to C bridge for gopher-orch FFI layer - * - * This header provides the internal bridge between C++ and C APIs with - * comprehensive RAII support, FFI-safe type conversions, and automatic - * resource management. It ensures thread-safe operations and prevents - * resource leaks through systematic RAII enforcement. - * - * Architecture: - * - RAII wrappers for all C++ resources - * - Thread-safe handle management with reference counting - * - Automatic cleanup through scope guards and transactions - * - FFI-safe type conversions with validation - * - Comprehensive error handling with recovery - * - * Key Design Decisions: - * - JSON-to-JSON type erasure at FFI boundary - * - All Runnable templates exposed as JsonRunnable - * - Thread-local error messages for C API - * - Opaque handles with reference counting - * - * This file is NOT part of the public API and should only be included - * by implementation files. - */ - -#ifndef GOPHER_ORCH_FFI_BRIDGE_H -#define GOPHER_ORCH_FFI_BRIDGE_H - -#include "orch_ffi.h" - -/* C++ standard library headers */ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* gopher-orch C++ headers */ -#include "gopher/orch/agent/agent.h" -#include "gopher/orch/callback/callback_handler.h" -#include "gopher/orch/callback/callback_manager.h" -#include "gopher/orch/core/config.h" -#include "gopher/orch/core/runnable.h" -#include "gopher/orch/core/types.h" -#include "gopher/orch/human/approval.h" - -/* mcp headers for dispatcher */ -#include "mcp/event/libevent_dispatcher.h" - -namespace gopher { -namespace orch { -namespace ffi { - -/* ============================================================================ - * Handle Base Class - * - * All FFI handle implementations derive from this base class. - * Provides reference counting and global registry for leak detection. - * ============================================================================ - */ - -class HandleBase { - public: - explicit HandleBase(gopher_orch_type_id_t type) - : ref_count_(1), type_id_(type) { - RegisterHandle(this); - } - - virtual ~HandleBase() { UnregisterHandle(this); } - - /* Reference counting */ - void AddRef() { ref_count_.fetch_add(1, std::memory_order_relaxed); } - - void Release() { - if (ref_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) { - delete this; - } - } - - int32_t GetRefCount() const { - return ref_count_.load(std::memory_order_relaxed); - } - - gopher_orch_type_id_t GetType() const { return type_id_; } - - /* Virtual methods for resource management */ - virtual void Cleanup() {} - virtual bool IsValid() const { return true; } - - private: - std::atomic ref_count_; - gopher_orch_type_id_t type_id_; - - /* Global handle registry */ - static void RegisterHandle(HandleBase* handle); - static void UnregisterHandle(HandleBase* handle); -}; - -/* ============================================================================ - * Handle Registry for Leak Detection - * ============================================================================ - */ - -class HandleRegistry { - public: - static HandleRegistry& Instance() { - static HandleRegistry instance; - return instance; - } - - void Register(HandleBase* handle) { - if (!handle) - return; - std::lock_guard lock(mutex_); - handles_.insert(handle); - stats_.total_created++; - } - - void Unregister(HandleBase* handle) { - if (!handle) - return; - std::lock_guard lock(mutex_); - handles_.erase(handle); - stats_.total_destroyed++; - } - - bool IsValid(void* handle) const { - if (!handle) - return false; - std::lock_guard lock(mutex_); - return handles_.find(static_cast(handle)) != handles_.end(); - } - - struct Stats { - size_t total_created{0}; - size_t total_destroyed{0}; - }; - - Stats GetStats() const { - std::lock_guard lock(mutex_); - return stats_; - } - - size_t GetActiveCount() const { - std::lock_guard lock(mutex_); - return handles_.size(); - } - - void PrintLeakReport() const { - std::lock_guard lock(mutex_); - if (!handles_.empty()) { - fprintf(stderr, "gopher-orch FFI: %zu handles leaked:\n", - handles_.size()); - for (auto* handle : handles_) { - fprintf(stderr, " - Handle type %d at %p (refcount=%d)\n", - handle->GetType(), static_cast(handle), - handle->GetRefCount()); - } - } - } - - private: - mutable std::mutex mutex_; - std::unordered_set handles_; - Stats stats_; -}; - -/* Inline implementations */ -inline void HandleBase::RegisterHandle(HandleBase* handle) { - HandleRegistry::Instance().Register(handle); -} - -inline void HandleBase::UnregisterHandle(HandleBase* handle) { - HandleRegistry::Instance().Unregister(handle); -} - -/* ============================================================================ - * Error Manager - Thread-local error handling - * ============================================================================ - */ - -class ErrorManager { - public: - static void SetError(gopher_orch_error_t code, - const std::string& message, - const std::string& details = "", - const char* file = nullptr, - int line = 0) { - auto& info = GetThreadLocalError(); - info.code = code; - - /* Store message in thread-local storage */ - auto& msg = GetThreadLocalMessage(); - auto& det = GetThreadLocalDetails(); - msg = message; - det = details; - - info.message = msg.c_str(); - info.details = det.empty() ? nullptr : det.c_str(); - info.file = file; - info.line = line; - } - - static const gopher_orch_error_info_t* GetLastError() { - auto& info = GetThreadLocalError(); - return (info.code != GOPHER_ORCH_OK) ? &info : nullptr; - } - - static void ClearError() { - auto& info = GetThreadLocalError(); - info.code = GOPHER_ORCH_OK; - info.message = nullptr; - info.details = nullptr; - info.file = nullptr; - info.line = 0; - } - - static const char* GetErrorName(gopher_orch_error_t code) { - switch (code) { - case GOPHER_ORCH_OK: - return "GOPHER_ORCH_OK"; - case GOPHER_ORCH_ERROR_INVALID_HANDLE: - return "GOPHER_ORCH_ERROR_INVALID_HANDLE"; - case GOPHER_ORCH_ERROR_INVALID_ARGUMENT: - return "GOPHER_ORCH_ERROR_INVALID_ARGUMENT"; - case GOPHER_ORCH_ERROR_NULL_POINTER: - return "GOPHER_ORCH_ERROR_NULL_POINTER"; - case GOPHER_ORCH_ERROR_NOT_FOUND: - return "GOPHER_ORCH_ERROR_NOT_FOUND"; - case GOPHER_ORCH_ERROR_ALREADY_EXISTS: - return "GOPHER_ORCH_ERROR_ALREADY_EXISTS"; - case GOPHER_ORCH_ERROR_RESOURCE_LIMIT: - return "GOPHER_ORCH_ERROR_RESOURCE_LIMIT"; - case GOPHER_ORCH_ERROR_NO_MEMORY: - return "GOPHER_ORCH_ERROR_NO_MEMORY"; - case GOPHER_ORCH_ERROR_CONNECTION_FAILED: - return "GOPHER_ORCH_ERROR_CONNECTION_FAILED"; - case GOPHER_ORCH_ERROR_NOT_CONNECTED: - return "GOPHER_ORCH_ERROR_NOT_CONNECTED"; - case GOPHER_ORCH_ERROR_TIMEOUT: - return "GOPHER_ORCH_ERROR_TIMEOUT"; - case GOPHER_ORCH_ERROR_INVALID_TRANSITION: - return "GOPHER_ORCH_ERROR_INVALID_TRANSITION"; - case GOPHER_ORCH_ERROR_GUARD_REJECTED: - return "GOPHER_ORCH_ERROR_GUARD_REJECTED"; - case GOPHER_ORCH_ERROR_INVALID_STATE: - return "GOPHER_ORCH_ERROR_INVALID_STATE"; - case GOPHER_ORCH_ERROR_CANCELLED: - return "GOPHER_ORCH_ERROR_CANCELLED"; - case GOPHER_ORCH_ERROR_APPROVAL_DENIED: - return "GOPHER_ORCH_ERROR_APPROVAL_DENIED"; - case GOPHER_ORCH_ERROR_CIRCUIT_OPEN: - return "GOPHER_ORCH_ERROR_CIRCUIT_OPEN"; - case GOPHER_ORCH_ERROR_FALLBACK_EXHAUSTED: - return "GOPHER_ORCH_ERROR_FALLBACK_EXHAUSTED"; - case GOPHER_ORCH_ERROR_PARSE_ERROR: - return "GOPHER_ORCH_ERROR_PARSE_ERROR"; - case GOPHER_ORCH_ERROR_INVALID_JSON: - return "GOPHER_ORCH_ERROR_INVALID_JSON"; - case GOPHER_ORCH_ERROR_INTERNAL: - return "GOPHER_ORCH_ERROR_INTERNAL"; - case GOPHER_ORCH_ERROR_NOT_IMPLEMENTED: - return "GOPHER_ORCH_ERROR_NOT_IMPLEMENTED"; - default: - return "GOPHER_ORCH_ERROR_UNKNOWN"; - } - } - - private: - static gopher_orch_error_info_t& GetThreadLocalError() { - thread_local gopher_orch_error_info_t info = {}; - return info; - } - - static std::string& GetThreadLocalMessage() { - thread_local std::string message; - return message; - } - - static std::string& GetThreadLocalDetails() { - thread_local std::string details; - return details; - } -}; - -/* Macro for setting error with file/line */ -#define SET_ERROR(code, msg) \ - ErrorManager::SetError(code, msg, "", __FILE__, __LINE__) - -#define SET_ERROR_DETAIL(code, msg, detail) \ - ErrorManager::SetError(code, msg, detail, __FILE__, __LINE__) - -/* ============================================================================ - * Handle Implementations - * ============================================================================ - */ - -/** - * JSON value handle implementation - */ -struct JsonImpl : public HandleBase { - explicit JsonImpl(core::JsonValue value) - : HandleBase(GOPHER_ORCH_TYPE_JSON), value(std::move(value)) {} - - core::JsonValue value; -}; - -/** - * Dispatcher handle implementation - * Uses LibeventDispatcher as the concrete implementation - */ -struct DispatcherImpl : public HandleBase { - DispatcherImpl() - : HandleBase(GOPHER_ORCH_TYPE_DISPATCHER), - dispatcher(std::make_unique("ffi")) {} - - ~DispatcherImpl() override { Cleanup(); } - - void Cleanup() override { - if (dispatcher) { - dispatcher->exit(); - } - } - - std::unique_ptr dispatcher; - std::thread::id dispatcher_thread_id; -}; - -/** - * Configuration handle implementation - */ -struct ConfigImpl : public HandleBase { - ConfigImpl() : HandleBase(GOPHER_ORCH_TYPE_CONFIG) {} - - core::RunnableConfig config; -}; - -/** - * Runnable handle implementation - type-erased to JSON->JSON - */ -struct RunnableImpl : public HandleBase { - using JsonRunnable = core::Runnable; - - explicit RunnableImpl(std::shared_ptr runnable) - : HandleBase(GOPHER_ORCH_TYPE_RUNNABLE), runnable(std::move(runnable)) {} - - std::shared_ptr runnable; -}; - -/** - * Agent handle implementation - wraps ReActAgent functionality - */ -struct AgentImpl : public HandleBase { - explicit AgentImpl(std::shared_ptr agent) - : HandleBase(GOPHER_ORCH_TYPE_AGENT), agent(std::move(agent)) {} - - std::shared_ptr agent; -}; - -/** - * Callback manager handle implementation - */ -struct CallbackManagerImpl : public HandleBase { - CallbackManagerImpl() - : HandleBase(GOPHER_ORCH_TYPE_CALLBACK_MANAGER), - manager(std::make_shared()) {} - - std::shared_ptr manager; -}; - -/** - * Approval handler handle implementation - */ -struct ApprovalHandlerImpl : public HandleBase { - explicit ApprovalHandlerImpl(std::shared_ptr handler) - : HandleBase(GOPHER_ORCH_TYPE_APPROVAL_HANDLER), - handler(std::move(handler)) {} - - std::shared_ptr handler; -}; - -/** - * Cancellation token implementation - */ -struct CancelTokenImpl : public HandleBase { - CancelTokenImpl() : HandleBase(GOPHER_ORCH_TYPE_CANCEL_TOKEN) {} - - std::atomic cancelled{false}; -}; - -/** - * Iterator implementation - * Stores a copy of the keys for object iteration since ObjectIterator - * doesn't support proper copy semantics - */ -struct IteratorImpl : public HandleBase { - IteratorImpl(gopher_orch_json_t json) - : HandleBase(GOPHER_ORCH_TYPE_ITERATOR), json_(json), index_(0) { - if (json) { - auto* impl = reinterpret_cast(json); - if (impl->value.isObject()) { - is_object_ = true; - /* Store all keys for iteration */ - object_keys_ = impl->value.keys(); - } else if (impl->value.isArray()) { - is_object_ = false; - array_size_ = impl->value.size(); - } - } - } - - gopher_orch_json_t json_; - size_t index_; - bool is_object_ = false; - std::vector object_keys_; - size_t array_size_ = 0; - std::string current_key_; - core::JsonValue current_value_; -}; - -/** - * Sequence builder implementation - */ -struct SequenceImpl : public HandleBase { - SequenceImpl() : HandleBase(GOPHER_ORCH_TYPE_SEQUENCE) {} - - std::vector> steps; -}; - -/** - * Parallel builder implementation - */ -struct ParallelImpl : public HandleBase { - ParallelImpl() : HandleBase(GOPHER_ORCH_TYPE_PARALLEL) {} - - std::vector< - std::pair>> - branches; -}; - -/** - * Router builder implementation - */ -struct RouterImpl : public HandleBase { - RouterImpl() : HandleBase(GOPHER_ORCH_TYPE_ROUTER) {} - - struct Route { - gopher_orch_condition_fn condition; - void* user_context; - std::shared_ptr runnable; - }; - - std::vector routes; - std::shared_ptr default_route; -}; - -/** - * RAII guard implementation - */ -struct GuardImpl : public HandleBase { - GuardImpl(void* handle, - gopher_orch_type_id_t type, - gopher_orch_cleanup_fn cleanup) - : HandleBase(GOPHER_ORCH_TYPE_GUARD), - handle_(handle), - type_(type), - cleanup_(cleanup), - released_(false) {} - - ~GuardImpl() override { - if (!released_ && handle_ && cleanup_) { - cleanup_(handle_); - } - } - - void* Release() { - void* h = handle_; - handle_ = nullptr; - released_ = true; - return h; - } - - void* handle_; - gopher_orch_type_id_t type_; - gopher_orch_cleanup_fn cleanup_; - bool released_; -}; - -/** - * Transaction implementation - */ -struct TransactionImpl : public HandleBase { - struct Resource { - void* handle; - gopher_orch_type_id_t type; - gopher_orch_cleanup_fn cleanup; - }; - - explicit TransactionImpl(const gopher_orch_transaction_opts_t* opts) - : HandleBase(GOPHER_ORCH_TYPE_TRANSACTION), committed_(false) { - if (opts) { - auto_rollback_ = opts->auto_rollback; - strict_ordering_ = opts->strict_ordering; - max_resources_ = opts->max_resources; - } - } - - ~TransactionImpl() override { - if (!committed_ && auto_rollback_) { - Rollback(); - } - } - - gopher_orch_error_t Add(void* handle, gopher_orch_type_id_t type) { - if (!handle) - return GOPHER_ORCH_ERROR_NULL_POINTER; - if (committed_) - return GOPHER_ORCH_ERROR_INVALID_STATE; - if (max_resources_ > 0 && resources_.size() >= max_resources_) - return GOPHER_ORCH_ERROR_RESOURCE_LIMIT; - - resources_.push_back({handle, type, nullptr}); - return GOPHER_ORCH_OK; - } - - gopher_orch_error_t Commit() { - if (committed_) - return GOPHER_ORCH_ERROR_INVALID_STATE; - committed_ = true; - resources_.clear(); - return GOPHER_ORCH_OK; - } - - void Rollback() { - if (committed_) - return; - - /* Cleanup in reverse order (LIFO) */ - while (!resources_.empty()) { - auto& res = resources_.back(); - CleanupResource(res); - resources_.pop_back(); - } - committed_ = true; - } - - size_t Size() const { return resources_.size(); } - - private: - void CleanupResource(const Resource& res) { - if (!res.handle) - return; - - if (res.cleanup) { - res.cleanup(res.handle); - } else { - /* Default cleanup based on type */ - auto* base = static_cast(res.handle); - base->Release(); - } - } - - std::vector resources_; - bool committed_; - bool auto_rollback_ = true; - bool strict_ordering_ = true; - size_t max_resources_ = 0; -}; - -/* ============================================================================ - * Lambda Runnable Implementation - * - * Wraps a C callback function as a JsonRunnable. - * ============================================================================ - */ - -class LambdaRunnable : public core::Runnable { - public: - LambdaRunnable(gopher_orch_lambda_fn fn, - void* user_context, - gopher_orch_destructor_fn destructor, - std::string name) - : fn_(fn), - user_context_(user_context), - destructor_(destructor), - name_(std::move(name)) {} - - ~LambdaRunnable() override { - if (destructor_ && user_context_) { - destructor_(user_context_); - } - } - - std::string name() const override { return name_; } - - void invoke(const core::JsonValue& input, - const core::RunnableConfig& config, - core::Dispatcher& dispatcher, - core::ResultCallback callback) override { - (void)config; - - /* Create input handle for callback */ - auto* input_impl = new JsonImpl(input); - - /* Post to dispatcher to call the callback in the right context */ - dispatcher.post([this, input_impl, callback]() { - gopher_orch_error_t error = GOPHER_ORCH_OK; - auto result = - fn_(user_context_, reinterpret_cast(input_impl), - &error); - - /* Cleanup input handle */ - input_impl->Release(); - - if (error != GOPHER_ORCH_OK || !result) { - callback(core::Result( - core::Error(error, ErrorManager::GetErrorName(error)))); - } else { - auto* result_impl = reinterpret_cast(result); - core::JsonValue output = result_impl->value; - result_impl->Release(); - callback(core::makeSuccess(std::move(output))); - } - }); - } - - private: - gopher_orch_lambda_fn fn_; - void* user_context_; - gopher_orch_destructor_fn destructor_; - std::string name_; -}; - -/* ============================================================================ - * FFI Callback Handler Implementation - * - * Wraps C callback functions as a CallbackHandler. - * ============================================================================ - */ - -class FFICallbackHandler : public callback::CallbackHandler { - public: - explicit FFICallbackHandler( - const gopher_orch_callback_handler_config_t& config) - : config_(config) {} - - ~FFICallbackHandler() override { - if (config_.destructor && config_.user_context) { - config_.destructor(config_.user_context); - } - } - - void onChainStart(const callback::RunInfo& info, - const core::JsonValue& input) override { - if (config_.on_chain_start) { - auto* input_impl = new JsonImpl(input); - config_.on_chain_start(config_.user_context, info.run_id.c_str(), - info.name.c_str(), - reinterpret_cast(input_impl)); - input_impl->Release(); - } - } - - void onChainEnd(const callback::RunInfo& info, - const core::JsonValue& output) override { - if (config_.on_chain_end) { - auto* output_impl = new JsonImpl(output); - config_.on_chain_end(config_.user_context, info.run_id.c_str(), - info.name.c_str(), - reinterpret_cast(output_impl)); - output_impl->Release(); - } - } - - void onChainError(const callback::RunInfo& info, - const core::Error& error) override { - if (config_.on_chain_error) { - config_.on_chain_error( - config_.user_context, info.run_id.c_str(), info.name.c_str(), - static_cast(error.code), error.message.c_str()); - } - } - - void onToolStart(const callback::RunInfo& info, - const std::string& tool_name, - const core::JsonValue& input) override { - if (config_.on_tool_start) { - auto* input_impl = new JsonImpl(input); - config_.on_tool_start(config_.user_context, info.run_id.c_str(), - tool_name.c_str(), - reinterpret_cast(input_impl)); - input_impl->Release(); - } - } - - void onToolEnd(const callback::RunInfo& info, - const std::string& tool_name, - const core::JsonValue& output) override { - if (config_.on_tool_end) { - auto* output_impl = new JsonImpl(output); - config_.on_tool_end(config_.user_context, info.run_id.c_str(), - tool_name.c_str(), - reinterpret_cast(output_impl)); - output_impl->Release(); - } - } - - void onToolError(const callback::RunInfo& info, - const std::string& tool_name, - const core::Error& error) override { - if (config_.on_tool_error) { - config_.on_tool_error( - config_.user_context, info.run_id.c_str(), tool_name.c_str(), - static_cast(error.code), error.message.c_str()); - } - } - - void onRetry(const callback::RunInfo& info, - const core::Error& error, - uint32_t attempt, - uint32_t max_attempts) override { - if (config_.on_retry) { - config_.on_retry( - config_.user_context, info.run_id.c_str(), info.name.c_str(), - static_cast(error.code), attempt, max_attempts); - } - } - - void onCustomEvent(const std::string& event_name, - const core::JsonValue& data) override { - if (config_.on_custom_event) { - auto* data_impl = new JsonImpl(data); - config_.on_custom_event(config_.user_context, event_name.c_str(), - reinterpret_cast(data_impl)); - data_impl->Release(); - } - } - - private: - gopher_orch_callback_handler_config_t config_; -}; - -/* ============================================================================ - * FFI Approval Handler Implementation - * - * Wraps C callback function as an ApprovalHandler. - * ============================================================================ - */ - -class FFIApprovalHandler : public human::ApprovalHandler { - public: - FFIApprovalHandler(gopher_orch_approval_fn fn, - void* user_context, - gopher_orch_destructor_fn destructor) - : fn_(fn), user_context_(user_context), destructor_(destructor) {} - - ~FFIApprovalHandler() override { - if (destructor_ && user_context_) { - destructor_(user_context_); - } - } - - void requestApproval( - const human::ApprovalRequest& request, - std::function callback) override { - /* Create preview handle */ - auto* preview_impl = new JsonImpl(request.preview); - - gopher_orch_bool_t approved = GOPHER_ORCH_FALSE; - char* reason = nullptr; - gopher_orch_json_t modifications = nullptr; - - fn_(user_context_, request.action_name.c_str(), - reinterpret_cast(preview_impl), - request.prompt.c_str(), &approved, &reason, &modifications); - - preview_impl->Release(); - - /* Build response */ - human::ApprovalResponse response; - response.approved = (approved != GOPHER_ORCH_FALSE); - response.reason = reason ? reason : ""; - - if (reason) { - gopher_orch_free(reason); - } - - if (modifications) { - auto* mod_impl = reinterpret_cast(modifications); - response.modifications = mod_impl->value; - mod_impl->Release(); - } - - callback(std::move(response)); - } - - private: - gopher_orch_approval_fn fn_; - void* user_context_; - gopher_orch_destructor_fn destructor_; -}; - -/* ============================================================================ - * Utility Macros for Handle Validation - * ============================================================================ - */ - -#define CHECK_HANDLE(handle, type_enum, return_val) \ - do { \ - if (!handle) { \ - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_HANDLE, "Handle is null"); \ - return return_val; \ - } \ - auto* base = reinterpret_cast(handle); \ - if (base->GetType() != type_enum) { \ - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_HANDLE, "Handle type mismatch"); \ - return return_val; \ - } \ - } while (0) - -#define CHECK_HANDLE_VOID(handle, type_enum) \ - do { \ - if (!handle) { \ - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_HANDLE, "Handle is null"); \ - return; \ - } \ - auto* base = reinterpret_cast(handle); \ - if (base->GetType() != type_enum) { \ - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_HANDLE, "Handle type mismatch"); \ - return; \ - } \ - } while (0) - -#define TRY_CATCH(code, return_val) \ - try { \ - code \ - } catch (const std::exception& e) { \ - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, e.what()); \ - return return_val; \ - } catch (...) { \ - SET_ERROR(GOPHER_ORCH_ERROR_UNKNOWN, "Unknown exception"); \ - return return_val; \ - } - -#define TRY_CATCH_VOID(code) \ - try { \ - code \ - } catch (const std::exception& e) { \ - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, e.what()); \ - return; \ - } catch (...) { \ - SET_ERROR(GOPHER_ORCH_ERROR_UNKNOWN, "Unknown exception"); \ - return; \ - } - -} // namespace ffi -} // namespace orch -} // namespace gopher - -#endif /* GOPHER_ORCH_FFI_BRIDGE_H */ diff --git a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_raii.h b/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_raii.h deleted file mode 100644 index e598cbc1..00000000 --- a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_raii.h +++ /dev/null @@ -1,554 +0,0 @@ -/** - * @file orch_ffi_raii.h - * @brief RAII utilities for gopher-orch C++ wrapper layer - * - * This header provides C++ RAII wrappers around the C FFI API, - * making it safe and convenient to use from C++ code while still - * going through the C API (useful for testing FFI bindings). - * - * These utilities follow the patterns from gopher-mcp C API: - * - ResourceGuard: RAII wrapper for single resources - * - AllocationTransaction: RAII wrapper for multi-resource transactions - * - ScopedCleanup: Execute cleanup on scope exit - * - * Usage: - * // Single resource with automatic cleanup - * auto json = ResourceGuard( - * gopher_orch_json_object(), - * gopher_orch_json_release); - * - * // Multi-resource transaction - * AllocationTransaction txn; - * txn.track(gopher_orch_json_object(), gopher_orch_json_release); - * txn.track(gopher_orch_json_array(), gopher_orch_json_release); - * // ... do work ... - * txn.commit(); // Ownership transferred, no cleanup on scope exit - */ - -#ifndef GOPHER_ORCH_FFI_RAII_H -#define GOPHER_ORCH_FFI_RAII_H - -#ifdef __cplusplus - -#include -#include -#include -#include -#include - -#include "orch_ffi.h" - -namespace gopher { -namespace orch { -namespace ffi { - -/* ============================================================================ - * ResourceGuard - RAII wrapper for single handle - * - * Similar to std::unique_ptr but designed for C FFI handles. - * ============================================================================ - */ - -template -class ResourceGuard { - public: - using Deleter = std::function; - - /* Default constructor - empty guard */ - ResourceGuard() : handle_(nullptr), deleter_(nullptr) {} - - /* Constructor with handle and deleter */ - ResourceGuard(T handle, Deleter deleter) - : handle_(handle), deleter_(std::move(deleter)) {} - - /* Move constructor */ - ResourceGuard(ResourceGuard&& other) noexcept - : handle_(other.handle_), deleter_(std::move(other.deleter_)) { - other.handle_ = nullptr; - } - - /* Move assignment */ - ResourceGuard& operator=(ResourceGuard&& other) noexcept { - if (this != &other) { - reset(); - handle_ = other.handle_; - deleter_ = std::move(other.deleter_); - other.handle_ = nullptr; - } - return *this; - } - - /* Disable copy */ - ResourceGuard(const ResourceGuard&) = delete; - ResourceGuard& operator=(const ResourceGuard&) = delete; - - /* Destructor - cleanup if not released */ - ~ResourceGuard() { reset(); } - - /* Get the underlying handle (does not transfer ownership) */ - T get() const { return handle_; } - - /* Implicit conversion to handle type for convenience */ - operator T() const { return handle_; } - - /* Check if guard holds a valid handle */ - explicit operator bool() const { return handle_ != nullptr; } - - /* Release ownership and return the handle */ - T release() { - T h = handle_; - handle_ = nullptr; - return h; - } - - /* Reset and cleanup current handle, optionally set new handle */ - void reset(T new_handle = nullptr, Deleter new_deleter = nullptr) { - if (handle_ && deleter_) { - deleter_(handle_); - } - handle_ = new_handle; - if (new_deleter) { - deleter_ = std::move(new_deleter); - } - } - - /* Swap with another guard */ - void swap(ResourceGuard& other) noexcept { - std::swap(handle_, other.handle_); - std::swap(deleter_, other.deleter_); - } - - private: - T handle_; - Deleter deleter_; -}; - -/* ============================================================================ - * Convenience type aliases for common handle types - * ============================================================================ - */ - -using JsonGuard = ResourceGuard; -using RunnableGuard = ResourceGuard; -using DispatcherGuard = ResourceGuard; -using ConfigGuard = ResourceGuard; -using ServerGuard = ResourceGuard; -using FsmGuard = ResourceGuard; -using GraphGuard = ResourceGuard; -using SequenceGuard = ResourceGuard; -using ParallelGuard = ResourceGuard; -using RouterGuard = ResourceGuard; -using CallbackManagerGuard = ResourceGuard; -using ApprovalHandlerGuard = ResourceGuard; -using CancelTokenGuard = ResourceGuard; -using IteratorGuard = ResourceGuard; - -/* ============================================================================ - * Factory functions for creating guarded handles - * ============================================================================ - */ - -inline JsonGuard make_json_null() { - return JsonGuard(gopher_orch_json_null(), gopher_orch_json_release); -} - -inline JsonGuard make_json_bool(gopher_orch_bool_t value) { - return JsonGuard(gopher_orch_json_bool(value), gopher_orch_json_release); -} - -inline JsonGuard make_json_int(int64_t value) { - return JsonGuard(gopher_orch_json_int(value), gopher_orch_json_release); -} - -inline JsonGuard make_json_double(double value) { - return JsonGuard(gopher_orch_json_double(value), gopher_orch_json_release); -} - -inline JsonGuard make_json_string(const char* value) { - return JsonGuard(gopher_orch_json_string(value), gopher_orch_json_release); -} - -inline JsonGuard make_json_object() { - return JsonGuard(gopher_orch_json_object(), gopher_orch_json_release); -} - -inline JsonGuard make_json_array() { - return JsonGuard(gopher_orch_json_array(), gopher_orch_json_release); -} - -inline JsonGuard parse_json(const char* json_str) { - return JsonGuard(gopher_orch_json_parse(json_str), gopher_orch_json_release); -} - -inline DispatcherGuard make_dispatcher() { - return DispatcherGuard(gopher_orch_dispatcher_create(), - gopher_orch_dispatcher_destroy); -} - -inline ConfigGuard make_config() { - return ConfigGuard(gopher_orch_config_create(), gopher_orch_config_destroy); -} - -inline SequenceGuard make_sequence() { - return SequenceGuard(gopher_orch_sequence_create(), - gopher_orch_sequence_destroy); -} - -inline ParallelGuard make_parallel() { - return ParallelGuard(gopher_orch_parallel_create(), - gopher_orch_parallel_destroy); -} - -inline RouterGuard make_router() { - return RouterGuard(gopher_orch_router_create(), gopher_orch_router_destroy); -} - -inline GraphGuard make_graph() { - return GraphGuard(gopher_orch_graph_create(), gopher_orch_graph_destroy); -} - -inline FsmGuard make_fsm(int32_t initial_state) { - return FsmGuard(gopher_orch_fsm_create(initial_state), - gopher_orch_fsm_destroy); -} - -inline CancelTokenGuard make_cancel_token() { - return CancelTokenGuard(gopher_orch_cancel_token_create(), - gopher_orch_cancel_token_destroy); -} - -inline CallbackManagerGuard make_callback_manager() { - return CallbackManagerGuard(gopher_orch_callback_manager_create(), - gopher_orch_callback_manager_destroy); -} - -/* ============================================================================ - * AllocationTransaction - RAII wrapper for multi-resource operations - * - * Ensures all-or-nothing semantics: if commit() is not called before - * destruction, all tracked resources are cleaned up. - * ============================================================================ - */ - -class AllocationTransaction { - public: - AllocationTransaction() : committed_(false) {} - - /* Disable copy */ - AllocationTransaction(const AllocationTransaction&) = delete; - AllocationTransaction& operator=(const AllocationTransaction&) = delete; - - /* Move support */ - AllocationTransaction(AllocationTransaction&& other) noexcept - : resources_(std::move(other.resources_)), committed_(other.committed_) { - other.committed_ = true; /* Prevent cleanup in moved-from object */ - } - - AllocationTransaction& operator=(AllocationTransaction&& other) noexcept { - if (this != &other) { - rollback(); - resources_ = std::move(other.resources_); - committed_ = other.committed_; - other.committed_ = true; - } - return *this; - } - - /* Destructor - rollback if not committed */ - ~AllocationTransaction() { - if (!committed_) { - rollback(); - } - } - - /** - * Track a resource for cleanup - * @param handle Resource handle - * @param deleter Cleanup function - */ - template - void track(T handle, D deleter) { - if (handle) { - resources_.emplace_back([handle, deleter]() { deleter(handle); }); - } - } - - /** - * Track a ResourceGuard (takes ownership) - */ - template - void track(ResourceGuard&& guard) { - if (guard) { - T handle = guard.release(); - /* Need to capture the deleter type-erased */ - resources_.emplace_back([handle]() { - /* This requires knowing the deleter type - use with care */ - /* For full type safety, use the track(handle, deleter) overload */ - }); - } - } - - /** - * Commit transaction - prevent cleanup - */ - void commit() { committed_ = true; } - - /** - * Rollback transaction - cleanup all resources - */ - void rollback() { - /* Cleanup in reverse order (LIFO) */ - while (!resources_.empty()) { - try { - resources_.back()(); - } catch (...) { - /* Suppress exceptions during cleanup */ - } - resources_.pop_back(); - } - committed_ = true; /* Prevent double cleanup */ - } - - /** - * Get number of tracked resources - */ - size_t size() const { return resources_.size(); } - - /** - * Check if transaction has been committed - */ - bool is_committed() const { return committed_; } - - private: - std::vector> resources_; - bool committed_; -}; - -/* ============================================================================ - * ScopedCleanup - Execute cleanup function on scope exit - * - * Use for any cleanup that doesn't fit the handle pattern. - * ============================================================================ - */ - -class ScopedCleanup { - public: - using Cleanup = std::function; - - explicit ScopedCleanup(Cleanup cleanup) - : cleanup_(std::move(cleanup)), dismissed_(false) {} - - /* Disable copy */ - ScopedCleanup(const ScopedCleanup&) = delete; - ScopedCleanup& operator=(const ScopedCleanup&) = delete; - - /* Move support */ - ScopedCleanup(ScopedCleanup&& other) noexcept - : cleanup_(std::move(other.cleanup_)), dismissed_(other.dismissed_) { - other.dismissed_ = true; - } - - ScopedCleanup& operator=(ScopedCleanup&& other) noexcept { - if (this != &other) { - execute(); - cleanup_ = std::move(other.cleanup_); - dismissed_ = other.dismissed_; - other.dismissed_ = true; - } - return *this; - } - - ~ScopedCleanup() { execute(); } - - /** - * Dismiss cleanup - prevent execution - */ - void dismiss() { dismissed_ = true; } - - /** - * Execute cleanup now (and dismiss) - */ - void execute() { - if (!dismissed_ && cleanup_) { - try { - cleanup_(); - } catch (...) { - /* Suppress exceptions */ - } - dismissed_ = true; - } - } - - private: - Cleanup cleanup_; - bool dismissed_; -}; - -/* Helper macro for scope cleanup */ -#define GOPHER_ORCH_SCOPE_EXIT(code) \ - ::gopher::orch::ffi::ScopedCleanup _scope_exit_##__LINE__([&]() { code; }) - -/* ============================================================================ - * ErrorScope - Clear error on scope entry, optionally check on exit - * ============================================================================ - */ - -class ErrorScope { - public: - ErrorScope() { gopher_orch_clear_error(); } - - ~ErrorScope() = default; - - /** - * Get last error code - */ - gopher_orch_error_t error() const { - auto info = gopher_orch_last_error(); - return info ? info->code : GOPHER_ORCH_OK; - } - - /** - * Get last error message - */ - const char* message() const { - auto info = gopher_orch_last_error(); - return info ? info->message : nullptr; - } - - /** - * Check if there was an error - */ - bool has_error() const { - auto info = gopher_orch_last_error(); - return info && info->code != GOPHER_ORCH_OK; - } - - /** - * Throw exception if there was an error - */ - void throw_if_error() const { - if (has_error()) { - throw std::runtime_error(message() ? message() : "Unknown error"); - } - } -}; - -/* ============================================================================ - * StringGuard - RAII wrapper for owned strings - * ============================================================================ - */ - -class StringGuard { - public: - StringGuard() : str_(nullptr) {} - explicit StringGuard(char* str) : str_(str) {} - - /* Disable copy */ - StringGuard(const StringGuard&) = delete; - StringGuard& operator=(const StringGuard&) = delete; - - /* Move support */ - StringGuard(StringGuard&& other) noexcept : str_(other.str_) { - other.str_ = nullptr; - } - - StringGuard& operator=(StringGuard&& other) noexcept { - if (this != &other) { - reset(); - str_ = other.str_; - other.str_ = nullptr; - } - return *this; - } - - ~StringGuard() { reset(); } - - const char* get() const { return str_; } - const char* c_str() const { return str_; } - operator const char*() const { return str_; } - explicit operator bool() const { return str_ != nullptr; } - - char* release() { - char* s = str_; - str_ = nullptr; - return s; - } - - void reset(char* new_str = nullptr) { - if (str_) { - gopher_orch_free(str_); - } - str_ = new_str; - } - - private: - char* str_; -}; - -/* Factory for JSON stringify */ -inline StringGuard stringify_json(gopher_orch_json_t json) { - return StringGuard(gopher_orch_json_stringify(json)); -} - -inline StringGuard stringify_json_pretty(gopher_orch_json_t json) { - return StringGuard(gopher_orch_json_stringify_pretty(json)); -} - -/* ============================================================================ - * Async completion helper - * ============================================================================ - */ - -/** - * SyncCompletion - Helper for blocking on async operations - * - * Usage: - * SyncCompletion completion; - * gopher_orch_runnable_invoke(runnable, input, config, dispatcher, - * nullptr, - * SyncCompletion::callback, &completion); - * dispatcher->run_until(completion.is_complete); - * auto result = completion.get_result(); - */ -template -class SyncCompletion { - public: - SyncCompletion() - : complete_(false), error_(GOPHER_ORCH_OK), result_(nullptr) {} - - /* Static callback for C API */ - static void callback(void* user_context, - gopher_orch_error_t error, - T result) noexcept { - auto* self = static_cast(user_context); - self->error_ = error; - self->result_ = result; - self->complete_ = true; - } - - bool is_complete() const { return complete_; } - gopher_orch_error_t error() const { return error_; } - T result() const { return result_; } - - /* Get result, taking ownership */ - T take_result() { - T r = result_; - result_ = nullptr; - return r; - } - - private: - std::atomic complete_; - gopher_orch_error_t error_; - T result_; -}; - -using JsonSyncCompletion = SyncCompletion; - -} // namespace ffi -} // namespace orch -} // namespace gopher - -#endif /* __cplusplus */ - -#endif /* GOPHER_ORCH_FFI_RAII_H */ diff --git a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_types.h b/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_types.h deleted file mode 100644 index b94b7ee0..00000000 --- a/third_party/gopher-orch/include/gopher/orch/ffi/orch_ffi_types.h +++ /dev/null @@ -1,561 +0,0 @@ -/** - * @file orch_ffi_types.h - * @brief FFI-safe type definitions for gopher-orch C API - * - * This header provides FFI-safe type definitions enabling gopher-orch to be - * used from any language with C FFI support (Python, Rust, Go, Node.js, etc.). - * - * Design Principles (following gopher-mcp C API patterns): - * - All types are FFI-safe primitives or opaque handles - * - Opaque handles hide C++ implementation details - * - Clear ownership semantics: OWNED vs BORROWED annotations - * - Thread-local error handling for non-intrusive error propagation - * - JSON-to-JSON as the primary FFI boundary (type-erased) - * - Callback convention: function pointer + void* context - * - * Architecture: - * - All operations happen in dispatcher thread context - * - Callbacks are invoked in dispatcher thread - * - RAII guards ensure automatic cleanup - * - Follows Create -> Configure -> Use -> Destroy lifecycle - * - * Memory Management: - * - All handles are reference-counted internally - * - Automatic cleanup through RAII guards - * - Optional manual resource management with explicit _free() functions - * - Thread-safe resource tracking in debug mode - */ - -#ifndef GOPHER_ORCH_FFI_TYPES_H -#define GOPHER_ORCH_FFI_TYPES_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* ============================================================================ - * Platform Detection and Export Macros - * ============================================================================ - */ - -#if defined(_WIN32) || defined(__CYGWIN__) -#ifdef GOPHER_ORCH_BUILDING_DLL -#define GOPHER_ORCH_API __declspec(dllexport) -#else -#define GOPHER_ORCH_API __declspec(dllimport) -#endif -#else -#if __GNUC__ >= 4 || defined(__clang__) -#define GOPHER_ORCH_API __attribute__((visibility("default"))) -#else -#define GOPHER_ORCH_API -#endif -#endif - -/* C++ noexcept compatibility */ -#ifdef __cplusplus -#define GOPHER_ORCH_NOEXCEPT noexcept -#else -#define GOPHER_ORCH_NOEXCEPT -#endif - -/* ============================================================================ - * FFI-Safe Primitive Types - * ============================================================================ - */ - -/** Boolean type - 0 = false, non-zero = true */ -typedef int32_t gopher_orch_bool_t; -#define GOPHER_ORCH_FALSE 0 -#define GOPHER_ORCH_TRUE 1 - -/** Size type for counts and lengths */ -typedef size_t gopher_orch_size_t; - -/** Duration in milliseconds */ -typedef uint64_t gopher_orch_duration_ms_t; - -/* ============================================================================ - * Opaque Handle Types - * - * All handles are pointers to implementation structs. - * NULL indicates invalid/error. - * Forward declarations hide C++ implementation details. - * - * Handles are reference-counted internally: - * - gopher_orch_*_add_ref() increments reference count - * - gopher_orch_*_release() decrements reference count - * - When count reaches 0, resource is destroyed - * ============================================================================ - */ - -/** Dispatcher handle - event loop for async operations */ -typedef struct gopher_orch_dispatcher_impl* gopher_orch_dispatcher_t; - -/** - * Runnable handle - type-erased JSON-to-JSON operation - * - * This is the core abstraction: all runnables are exposed as JSON->JSON - * transformations at the FFI boundary, regardless of their C++ template types. - */ -typedef struct gopher_orch_runnable_impl* gopher_orch_runnable_t; - -/** Server handle - MCP server connection */ -typedef struct gopher_orch_server_impl* gopher_orch_server_t; - -/** Agent handle - ReActAgent wrapper for AI orchestration */ -typedef struct gopher_orch_agent_impl* gopher_orch_agent_t; - -/** JSON value handle - wrapper around internal JSON type */ -typedef struct gopher_orch_json_impl* gopher_orch_json_t; - -/** Configuration handle - RunnableConfig wrapper */ -typedef struct gopher_orch_config_impl* gopher_orch_config_t; - -/** Callback manager handle - for observability */ -typedef struct gopher_orch_callback_manager_impl* - gopher_orch_callback_manager_t; - -/** Approval handler handle - for human-in-the-loop */ -typedef struct gopher_orch_approval_handler_impl* - gopher_orch_approval_handler_t; - -/** Sequence builder handle */ -typedef struct gopher_orch_sequence_impl* gopher_orch_sequence_t; - -/** Parallel builder handle */ -typedef struct gopher_orch_parallel_impl* gopher_orch_parallel_t; - -/** Router builder handle */ -typedef struct gopher_orch_router_impl* gopher_orch_router_t; - -/** State machine handle */ -typedef struct gopher_orch_fsm_impl* gopher_orch_fsm_t; - -/** State graph builder handle */ -typedef struct gopher_orch_graph_impl* gopher_orch_graph_t; - -/** Compiled state graph handle (runnable) */ -typedef struct gopher_orch_compiled_graph_impl* gopher_orch_compiled_graph_t; - -/** Cancellation token handle */ -typedef struct gopher_orch_cancel_token_impl* gopher_orch_cancel_token_t; - -/** Iterator handle - for collections */ -typedef struct gopher_orch_iterator_impl* gopher_orch_iterator_t; - -/** RAII guard handle - for automatic cleanup */ -typedef struct gopher_orch_guard_impl* gopher_orch_guard_t; - -/** Transaction handle - for atomic multi-resource operations */ -typedef struct gopher_orch_transaction_impl* gopher_orch_transaction_t; - -/* ============================================================================ - * Type ID Enumeration - * - * Used for runtime type checking and RAII guard type validation. - * ============================================================================ - */ - -typedef enum { - GOPHER_ORCH_TYPE_UNKNOWN = 0, - GOPHER_ORCH_TYPE_DISPATCHER = 1, - GOPHER_ORCH_TYPE_RUNNABLE = 2, - GOPHER_ORCH_TYPE_SERVER = 3, - GOPHER_ORCH_TYPE_AGENT = 4, - GOPHER_ORCH_TYPE_JSON = 5, - GOPHER_ORCH_TYPE_CONFIG = 6, - GOPHER_ORCH_TYPE_CALLBACK_MANAGER = 7, - GOPHER_ORCH_TYPE_APPROVAL_HANDLER = 8, - GOPHER_ORCH_TYPE_SEQUENCE = 9, - GOPHER_ORCH_TYPE_PARALLEL = 10, - GOPHER_ORCH_TYPE_ROUTER = 11, - GOPHER_ORCH_TYPE_FSM = 12, - GOPHER_ORCH_TYPE_GRAPH = 13, - GOPHER_ORCH_TYPE_COMPILED_GRAPH = 14, - GOPHER_ORCH_TYPE_CANCEL_TOKEN = 15, - GOPHER_ORCH_TYPE_ITERATOR = 16, - GOPHER_ORCH_TYPE_GUARD = 17, - GOPHER_ORCH_TYPE_TRANSACTION = 18, -} gopher_orch_type_id_t; - -/* ============================================================================ - * Error Codes - * - * Negative values indicate errors, zero indicates success. - * Use gopher_orch_last_error() for detailed error information. - * ============================================================================ - */ - -typedef enum { - /* Success */ - GOPHER_ORCH_OK = 0, - - /* Handle/argument errors */ - GOPHER_ORCH_ERROR_INVALID_HANDLE = -1, - GOPHER_ORCH_ERROR_INVALID_ARGUMENT = -2, - GOPHER_ORCH_ERROR_NULL_POINTER = -3, - - /* Resource errors */ - GOPHER_ORCH_ERROR_NOT_FOUND = -10, - GOPHER_ORCH_ERROR_ALREADY_EXISTS = -11, - GOPHER_ORCH_ERROR_RESOURCE_LIMIT = -12, - GOPHER_ORCH_ERROR_NO_MEMORY = -13, - - /* Connection errors */ - GOPHER_ORCH_ERROR_CONNECTION_FAILED = -20, - GOPHER_ORCH_ERROR_NOT_CONNECTED = -21, - GOPHER_ORCH_ERROR_TIMEOUT = -22, - - /* State machine errors */ - GOPHER_ORCH_ERROR_INVALID_TRANSITION = -30, - GOPHER_ORCH_ERROR_GUARD_REJECTED = -31, - GOPHER_ORCH_ERROR_INVALID_STATE = -32, - - /* Execution errors */ - GOPHER_ORCH_ERROR_CANCELLED = -40, - GOPHER_ORCH_ERROR_APPROVAL_DENIED = -41, - GOPHER_ORCH_ERROR_CIRCUIT_OPEN = -42, - GOPHER_ORCH_ERROR_FALLBACK_EXHAUSTED = -43, - - /* Parse/format errors */ - GOPHER_ORCH_ERROR_PARSE_ERROR = -50, - GOPHER_ORCH_ERROR_INVALID_JSON = -51, - - /* Internal errors */ - GOPHER_ORCH_ERROR_INTERNAL = -90, - GOPHER_ORCH_ERROR_NOT_IMPLEMENTED = -91, - GOPHER_ORCH_ERROR_UNKNOWN = -99 -} gopher_orch_error_t; - -/* ============================================================================ - * Structured Error Information - * - * Provides detailed error context via thread-local storage. - * Error messages are valid until the next API call on the same thread. - * ============================================================================ - */ - -typedef struct { - gopher_orch_error_t code; /* Error code */ - const char* message; /* BORROWED: Error message, valid until next call */ - const char* details; /* BORROWED: Additional context, may be NULL */ - const char* file; /* BORROWED: Source file where error occurred */ - int32_t line; /* Source line number */ -} gopher_orch_error_info_t; - -/* ============================================================================ - * FFI-Safe String Types - * - * Strings are passed as const char* (null-terminated, UTF-8 encoded). - * For strings returned by the API: - * - BORROWED: Valid until next API call or handle destruction - * - OWNED: Caller must free with gopher_orch_free() - * ============================================================================ - */ - -/** Non-owning string view for input parameters */ -typedef struct { - const char* data; /* UTF-8 encoded, may be NULL */ - gopher_orch_size_t length; /* Length in bytes (excluding null terminator) */ -} gopher_orch_string_view_t; - -/** Owning string buffer for output parameters */ -typedef struct { - char* data; /* UTF-8 encoded, null-terminated */ - gopher_orch_size_t length; /* Length in bytes (excluding null terminator) */ - gopher_orch_size_t capacity; /* Allocated capacity */ -} gopher_orch_string_buffer_t; - -/* ============================================================================ - * Callback Function Types - * - * All callbacks follow the pattern: function pointer + void* user_context - * Callbacks are ALWAYS invoked in the dispatcher thread context. - * - * Convention for JSON callbacks (following the FFI analysis): - * (const char* input_json, void* context) -> char* - * But we use gopher_orch_json_t handles for efficiency (avoid re-parsing). - * ============================================================================ - */ - -/** - * Generic work callback - posted to dispatcher thread - * @param user_context User-provided context data - * - * Note: noexcept is not valid on typedef function pointers in C++14. - * Callbacks should not throw exceptions across the FFI boundary. - */ -typedef void (*gopher_orch_work_fn)(void* user_context); - -/** - * Destructor callback - called when callback registration is removed - * @param user_context User-provided context to cleanup - */ -typedef void (*gopher_orch_destructor_fn)(void* user_context); - -/** - * Async completion callback for JSON results - * OWNERSHIP: result is OWNED by callback - must call gopher_orch_json_release - * - * @param user_context User-provided context data - * @param error Error code (GOPHER_ORCH_OK on success) - * @param result JSON result handle, NULL on error, OWNED by callback - */ -typedef void (*gopher_orch_completion_fn)(void* user_context, - gopher_orch_error_t error, - gopher_orch_json_t result); - -/** - * State transition observer callback - * - * @param user_context User-provided context data - * @param from_state Previous state ID - * @param to_state New state ID - * @param event Triggering event ID - */ -typedef void (*gopher_orch_transition_fn)(void* user_context, - int32_t from_state, - int32_t to_state, - int32_t event); - -/** - * State machine guard callback - return non-zero to allow transition - * - * @param user_context User-provided context data - * @param from_state Current state ID - * @param event Triggering event ID - * @return Non-zero to allow transition, zero to reject - */ -typedef int32_t (*gopher_orch_guard_fn)(void* user_context, - int32_t from_state, - int32_t event); - -/** - * State machine action callback - * - * @param user_context User-provided context data - * @param from_state Previous state ID - * @param to_state New state ID - * @param event Triggering event ID - */ -typedef void (*gopher_orch_action_fn)(void* user_context, - int32_t from_state, - int32_t to_state, - int32_t event); - -/** - * Router condition callback - return non-zero if route should be taken - * - * @param user_context User-provided context data - * @param input Input JSON value, BORROWED - do not destroy - * @return Non-zero if this route should be taken - */ -typedef int32_t (*gopher_orch_condition_fn)(void* user_context, - gopher_orch_json_t input); - -/** - * StateGraph conditional edge callback - returns destination node name - * OWNERSHIP: Returned string is BORROWED - valid only during callback - * - * @param user_context User-provided context data - * @param state Current graph state, BORROWED - do not destroy - * @return Destination node name, BORROWED, or NULL to end - */ -typedef const char* (*gopher_orch_edge_condition_fn)(void* user_context, - gopher_orch_json_t state); - -/** - * Lambda function for custom runnables - * OWNERSHIP: input is BORROWED, return value is OWNED by caller - * - * This is the core FFI pattern: (JSON input, context) -> JSON output - * - * @param user_context User-provided context data - * @param input Input JSON value, BORROWED - do not destroy - * @param out_error Output error code - * @return Result JSON value, OWNED by caller, NULL on error - */ -typedef gopher_orch_json_t (*gopher_orch_lambda_fn)( - void* user_context, - gopher_orch_json_t input, - gopher_orch_error_t* out_error); - -/** - * Approval request callback for human-in-the-loop - * - * @param user_context User-provided context data - * @param action_name Name of the action requiring approval, BORROWED - * @param preview Preview data for review, BORROWED - do not destroy - * @param prompt Human-readable prompt, BORROWED - * @param out_approved Output: set to non-zero to approve - * @param out_reason Output: reason for decision, OWNED by caller (must free) - * @param out_modifications Output: optional input modifications, OWNED (may be - * NULL) - */ -typedef void (*gopher_orch_approval_fn)(void* user_context, - const char* action_name, - gopher_orch_json_t preview, - const char* prompt, - gopher_orch_bool_t* out_approved, - char** out_reason, - gopher_orch_json_t* out_modifications); - -/** - * Chain start/end event callback - * - * @param user_context User-provided context data - * @param run_id Unique run identifier, BORROWED - * @param name Chain name, BORROWED - * @param data Input/output data, BORROWED - do not destroy - */ -typedef void (*gopher_orch_chain_event_fn)(void* user_context, - const char* run_id, - const char* name, - gopher_orch_json_t data); - -/** - * Chain error event callback - */ -typedef void (*gopher_orch_chain_error_fn)(void* user_context, - const char* run_id, - const char* name, - gopher_orch_error_t error, - const char* message); - -/** - * Tool start/end event callback - */ -typedef void (*gopher_orch_tool_event_fn)(void* user_context, - const char* run_id, - const char* tool_name, - gopher_orch_json_t data); - -/** - * Tool error event callback - */ -typedef void (*gopher_orch_tool_error_fn)(void* user_context, - const char* run_id, - const char* tool_name, - gopher_orch_error_t error, - const char* message); - -/** - * Retry event callback - */ -typedef void (*gopher_orch_retry_fn)(void* user_context, - const char* run_id, - const char* name, - gopher_orch_error_t error, - uint32_t attempt, - uint32_t max_attempts); - -/** - * Custom event callback - */ -typedef void (*gopher_orch_custom_event_fn)(void* user_context, - const char* event_name, - gopher_orch_json_t data); - -/** - * Guard cleanup callback for RAII guards - * - * @param resource Resource to cleanup - */ -typedef void (*gopher_orch_cleanup_fn)(void* resource); - -/* ============================================================================ - * Configuration Structures - * ============================================================================ - */ - -/** Retry policy configuration */ -typedef struct { - uint32_t max_attempts; /* Maximum number of attempts (1 = no retry) */ - uint64_t initial_delay_ms; /* Initial delay between retries */ - double backoff_multiplier; /* Multiplier for exponential backoff */ - uint64_t max_delay_ms; /* Maximum delay between retries */ - gopher_orch_bool_t jitter; /* Add random jitter to delays */ -} gopher_orch_retry_policy_t; - -/** Circuit breaker policy configuration */ -typedef struct { - uint32_t failure_threshold; /* Failures before opening circuit */ - uint64_t recovery_timeout_ms; /* Time before attempting half-open */ - uint32_t half_open_max_calls; /* Max calls in half-open state */ -} gopher_orch_circuit_breaker_policy_t; - -/** MCP server transport type */ -typedef enum { - GOPHER_ORCH_TRANSPORT_STDIO = 0, - GOPHER_ORCH_TRANSPORT_SSE = 1, - GOPHER_ORCH_TRANSPORT_WEBSOCKET = 2 -} gopher_orch_transport_type_t; - -/** MCP server configuration */ -typedef struct { - const char* name; /* Server name */ - gopher_orch_transport_type_t transport; - - /* Stdio transport options */ - const char* command; /* Command to execute */ - const char* const* args; /* Command arguments (NULL-terminated) */ - gopher_orch_size_t args_count; - const char* const* env_keys; /* Environment variable keys */ - const char* const* env_values; /* Environment variable values */ - gopher_orch_size_t env_count; - - /* SSE/WebSocket transport options */ - const char* url; - const char* const* header_keys; - const char* const* header_values; - gopher_orch_size_t header_count; - - /* Timeouts */ - uint64_t connect_timeout_ms; - uint64_t request_timeout_ms; -} gopher_orch_mcp_config_t; - -/** Callback handler configuration */ -typedef struct { - gopher_orch_chain_event_fn on_chain_start; - gopher_orch_chain_event_fn on_chain_end; - gopher_orch_chain_error_fn on_chain_error; - gopher_orch_tool_event_fn on_tool_start; - gopher_orch_tool_event_fn on_tool_end; - gopher_orch_tool_error_fn on_tool_error; - gopher_orch_retry_fn on_retry; - gopher_orch_custom_event_fn on_custom_event; - void* user_context; - gopher_orch_destructor_fn destructor; /* Called when handler is removed */ -} gopher_orch_callback_handler_config_t; - -/** Transaction options */ -typedef struct { - gopher_orch_bool_t auto_rollback; /* Auto-rollback if not committed */ - gopher_orch_bool_t strict_ordering; /* Cleanup in reverse order (LIFO) */ - uint32_t max_resources; /* Maximum resources (0 = unlimited) */ -} gopher_orch_transaction_opts_t; - -/** State graph node configuration */ -typedef struct { - const char* name; /* Node name */ - gopher_orch_runnable_t runnable; /* Associated runnable (may be NULL) */ - const char* output_key; /* Key to write output to state (NULL for none) */ -} gopher_orch_node_config_t; - -/** State channel type for reducers */ -typedef enum { - GOPHER_ORCH_CHANNEL_LAST_VALUE = 0, /* Keep last value */ - GOPHER_ORCH_CHANNEL_APPEND_LIST = 1, /* Append to list */ - GOPHER_ORCH_CHANNEL_MERGE_OBJECT = 2, /* Merge objects */ -} gopher_orch_channel_type_t; - -#ifdef __cplusplus -} -#endif - -#endif /* GOPHER_ORCH_FFI_TYPES_H */ diff --git a/third_party/gopher-orch/include/gopher/orch/fsm/state_machine.h b/third_party/gopher-orch/include/gopher/orch/fsm/state_machine.h deleted file mode 100644 index 09bff002..00000000 --- a/third_party/gopher-orch/include/gopher/orch/fsm/state_machine.h +++ /dev/null @@ -1,335 +0,0 @@ -#pragma once - -// StateMachine - Type-safe finite state machine -// Manages entity lifecycles with discrete states and event-driven transitions -// -// Use cases: -// - Connection states (DISCONNECTED → CONNECTING → CONNECTED → ERROR) -// - Workflow lifecycle (PENDING → RUNNING → PAUSED → COMPLETED) -// - Agent behavior (IDLE → THINKING → ACTING → WAITING) - -#include -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { -namespace fsm { - -using namespace gopher::orch::core; - -// ============================================================================= -// StateMachine - Type-safe finite state machine -// ============================================================================= - -template -class StateMachine { - public: - using StateType = TState; - using EventType = TEvent; - using ContextType = TContext; - - // Guard: returns true if transition is allowed - using Guard = - std::function; - - // Action: executed during transition - using TransitionAction = - std::function; - - // State callbacks: executed on entry/exit - using StateAction = std::function; - - // Observer: notified of all state changes - using StateObserver = - std::function; - - // Async transition callback - using TransitionCallback = std::function)>; - - explicit StateMachine(TState initial_state) : current_state_(initial_state) {} - - // ========================================================================= - // Configuration (Builder pattern) - // ========================================================================= - - // Add a valid transition: from --[event]--> to - StateMachine& addTransition(TState from, TEvent event, TState to) { - transitions_[{from, event}] = to; - return *this; - } - - // Add guard condition for a transition - // Guard must return true for transition to proceed - StateMachine& setGuard(TState from, TEvent event, Guard guard) { - guards_[{from, event}] = std::move(guard); - return *this; - } - - // Add action to execute during transition (after exit, before enter) - StateMachine& setAction(TState from, TEvent event, TransitionAction action) { - actions_[{from, event}] = std::move(action); - return *this; - } - - // Set callback when entering a state - StateMachine& onEnter(TState state, StateAction callback) { - on_enter_[state] = std::move(callback); - return *this; - } - - // Set callback when exiting a state - StateMachine& onExit(TState state, StateAction callback) { - on_exit_[state] = std::move(callback); - return *this; - } - - // Set global state change observer (for logging/tracing) - StateMachine& onStateChange(StateObserver observer) { - state_observer_ = std::move(observer); - return *this; - } - - // ========================================================================= - // State Query - // ========================================================================= - - TState currentState() const { return current_state_; } - - bool isInState(TState state) const { return current_state_ == state; } - - // Check if an event can trigger a transition from current state - bool canTrigger(TEvent event) const { - return canTriggerWith(event, context_); - } - - bool canTriggerWith(TEvent event, const TContext& ctx) const { - auto key = std::make_pair(current_state_, event); - - // Check if transition exists - auto trans_it = transitions_.find(key); - if (trans_it == transitions_.end()) { - return false; - } - - // Check guard if present - auto guard_it = guards_.find(key); - if (guard_it != guards_.end()) { - return guard_it->second(current_state_, event, ctx); - } - - return true; - } - - // Get list of valid events from current state - std::vector validEvents() const { - std::vector events; - for (const auto& entry : transitions_) { - if (entry.first.first == current_state_) { - if (canTrigger(entry.first.second)) { - events.push_back(entry.first.second); - } - } - } - return events; - } - - // ========================================================================= - // Synchronous Trigger - // ========================================================================= - - Result trigger(TEvent event) { return triggerWith(event, context_); } - - Result triggerWith(TEvent event, TContext& ctx) { - auto key = std::make_pair(current_state_, event); - - // Find transition - auto trans_it = transitions_.find(key); - if (trans_it == transitions_.end()) { - return makeOrchError( - OrchError::INVALID_TRANSITION, - "No transition defined for event in current state"); - } - - // Check guard - auto guard_it = guards_.find(key); - if (guard_it != guards_.end() && - !guard_it->second(current_state_, event, ctx)) { - return makeOrchError(OrchError::GUARD_REJECTED, - "Transition guard returned false"); - } - - TState from_state = current_state_; - TState to_state = trans_it->second; - - // Execute exit callback - auto exit_it = on_exit_.find(from_state); - if (exit_it != on_exit_.end()) { - exit_it->second(from_state, ctx); - } - - // Execute transition action - auto action_it = actions_.find(key); - if (action_it != actions_.end()) { - action_it->second(from_state, to_state, event, ctx); - } - - // Update state - current_state_ = to_state; - - // Execute enter callback - auto enter_it = on_enter_.find(to_state); - if (enter_it != on_enter_.end()) { - enter_it->second(to_state, ctx); - } - - // Notify observer - if (state_observer_) { - state_observer_(from_state, to_state, event); - } - - return makeSuccess(to_state); - } - - // ========================================================================= - // Async Trigger (Dispatcher Integration) - // ========================================================================= - - void triggerAsync(TEvent event, - Dispatcher& dispatcher, - TransitionCallback callback) { - dispatcher.post([this, event, callback = std::move(callback)]() { - callback(trigger(event)); - }); - } - - void triggerAsyncWith(TEvent event, - TContext& ctx, - Dispatcher& dispatcher, - TransitionCallback callback) { - dispatcher.post([this, event, &ctx, callback = std::move(callback)]() { - callback(triggerWith(event, ctx)); - }); - } - - // ========================================================================= - // Context Management - // ========================================================================= - - void setContext(TContext ctx) { context_ = std::move(ctx); } - TContext& context() { return context_; } - const TContext& context() const { return context_; } - - // ========================================================================= - // Reset - // ========================================================================= - - void reset(TState state) { current_state_ = state; } - - void reset(TState state, TContext ctx) { - current_state_ = state; - context_ = std::move(ctx); - } - - private: - using TransitionKey = std::pair; - - // Custom comparator for pair keys (works with enums) - struct PairCompare { - bool operator()(const TransitionKey& a, const TransitionKey& b) const { - if (static_cast(a.first) != static_cast(b.first)) { - return static_cast(a.first) < static_cast(b.first); - } - return static_cast(a.second) < static_cast(b.second); - } - }; - - TState current_state_; - TContext context_; - - std::map transitions_; - std::map guards_; - std::map actions_; - std::map on_enter_; - std::map on_exit_; - StateObserver state_observer_; -}; - -// ============================================================================= -// StateMachineBuilder - Fluent builder for state machines -// ============================================================================= - -template -class StateMachineBuilder { - public: - using Machine = StateMachine; - - explicit StateMachineBuilder(TState initial_state) - : machine_(std::make_shared(initial_state)) {} - - // Add a transition - StateMachineBuilder& transition(TState from, TEvent event, TState to) { - machine_->addTransition(from, event, to); - return *this; - } - - // Set guard for last added transition - StateMachineBuilder& withGuard(TState from, - TEvent event, - typename Machine::Guard guard) { - machine_->setGuard(from, event, std::move(guard)); - return *this; - } - - // Set action for last added transition - StateMachineBuilder& withAction(TState from, - TEvent event, - typename Machine::TransitionAction action) { - machine_->setAction(from, event, std::move(action)); - return *this; - } - - // Set entry callback for a state - StateMachineBuilder& onEnter(TState state, - typename Machine::StateAction callback) { - machine_->onEnter(state, std::move(callback)); - return *this; - } - - // Set exit callback for a state - StateMachineBuilder& onExit(TState state, - typename Machine::StateAction callback) { - machine_->onExit(state, std::move(callback)); - return *this; - } - - // Set state change observer - StateMachineBuilder& onStateChange(typename Machine::StateObserver observer) { - machine_->onStateChange(std::move(observer)); - return *this; - } - - // Build the state machine - std::shared_ptr build() { return machine_; } - - // Implicit conversion - operator std::shared_ptr() { return build(); } - - private: - std::shared_ptr machine_; -}; - -// Factory function for creating state machine builder -template -StateMachineBuilder makeStateMachine( - TState initial_state) { - return StateMachineBuilder(initial_state); -} - -} // namespace fsm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/graph/compiled_graph.h b/third_party/gopher-orch/include/gopher/orch/graph/compiled_graph.h deleted file mode 100644 index 546377b7..00000000 --- a/third_party/gopher-orch/include/gopher/orch/graph/compiled_graph.h +++ /dev/null @@ -1,190 +0,0 @@ -#pragma once - -// CompiledStateGraph - Executable state graph -// -// Implements the Pregel model execution: -// 1. PLAN: Determine which nodes can execute -// 2. EXECUTE: Run scheduled nodes -// 3. UPDATE: Apply state changes atomically, prepare next step -// -// Design principles: -// - Async execution through dispatcher -// - Maximum iteration protection to prevent infinite loops -// - Clean error propagation -// - Composable with other Runnables via Runnable interface - -#include -#include -#include - -#include "gopher/orch/core/runnable.h" -#include "gopher/orch/graph/graph_node.h" -#include "gopher/orch/graph/graph_state.h" - -namespace gopher { -namespace orch { -namespace graph { - -// ============================================================================= -// CompiledStateGraph - Executable state graph (Runnable implementation) -// ============================================================================= -// -// CompiledStateGraph is created by calling StateGraph::compile(). -// It implements the Runnable interface, allowing it to be composed with -// other runnables (Sequence, Parallel, Router, etc.). -// -// Execution model: -// - Takes JsonValue input, converts to GraphState -// - Executes nodes following edges until END is reached -// - Returns final GraphState as JsonValue -// -// Error handling: -// - Node errors propagate immediately, stopping execution -// - Missing entry point or nodes are validation errors -// - Maximum iterations exceeded is a runtime error - -class CompiledStateGraph - : public core::Runnable { - public: - using EdgeCondition = std::function; - - // Maximum number of node executions before aborting - // Prevents infinite loops in cyclic graphs - static constexpr size_t MAX_ITERATIONS = 100; - - // Special node name indicating graph termination - // Using static method for C++14 compatibility - static const std::string& END() { - static const std::string end_node = "__end__"; - return end_node; - } - - // Special node name indicating graph start (entry point marker) - static const std::string& START() { - static const std::string start_node = "__start__"; - return start_node; - } - - // Construct from graph components - // Should only be called by StateGraph::compile() - CompiledStateGraph(std::map> nodes, - std::map edges, - std::map conditional_edges, - std::string entry_point) - : nodes_(std::move(nodes)), - edges_(std::move(edges)), - conditional_edges_(std::move(conditional_edges)), - entry_point_(std::move(entry_point)) {} - - std::string name() const override { return "CompiledStateGraph"; } - - void invoke(const core::JsonValue& input, - const core::RunnableConfig& config, - core::Dispatcher& dispatcher, - Callback callback) override { - if (entry_point_.empty()) { - dispatcher.post([callback = std::move(callback)]() { - callback(core::makeOrchError( - core::OrchError::INVALID_ARGUMENT, - "StateGraph entry point not set")); - }); - return; - } - - // Initialize state from input - GraphState initial_state = GraphState::fromJson(input); - - // Start execution from entry point - executeNode(entry_point_, initial_state, config, dispatcher, 0, - std::move(callback)); - } - - private: - // Execute a single node and continue to the next - // This is the core Pregel step implementation - void executeNode(const std::string& node_name, - const GraphState& state, - const core::RunnableConfig& config, - core::Dispatcher& dispatcher, - size_t iteration, - Callback callback) { - // Check termination conditions - if (node_name.empty() || node_name == END()) { - dispatcher.post([state, callback = std::move(callback)]() { - callback(core::makeSuccess(state.toJson())); - }); - return; - } - - // Guard against infinite loops - if (iteration >= MAX_ITERATIONS) { - dispatcher.post([callback = std::move(callback)]() { - callback(core::makeOrchError( - core::OrchError::INTERNAL_ERROR, "Maximum iterations exceeded")); - }); - return; - } - - // Find the node to execute - auto it = nodes_.find(node_name); - if (it == nodes_.end()) { - dispatcher.post([node_name, callback = std::move(callback)]() { - callback(core::makeOrchError( - core::OrchError::INVALID_ARGUMENT, "Node not found: " + node_name)); - }); - return; - } - - // Execute the node asynchronously - // Capture self via shared_ptr to extend lifetime through callbacks - auto self = - std::static_pointer_cast(shared_from_this()); - - it->second->invoke( - state, config.child(), dispatcher, - [self, node_name, config, &dispatcher, iteration, - callback = std::move(callback)](Result result) mutable { - if (mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - return; - } - - // Get updated state and determine next node - const auto& new_state = mcp::get(result); - std::string next_node = self->getNextNode(node_name, new_state); - - // Continue execution with the next node - self->executeNode(next_node, new_state, config, dispatcher, - iteration + 1, std::move(callback)); - }); - } - - // Determine the next node to execute based on edges - // Priority: conditional edges > direct edges > END - std::string getNextNode(const std::string& from, - const GraphState& state) const { - // Check conditional edges first (higher priority) - auto cond_it = conditional_edges_.find(from); - if (cond_it != conditional_edges_.end()) { - return cond_it->second(state); - } - - // Fall back to direct edges - auto edge_it = edges_.find(from); - if (edge_it != edges_.end()) { - return edge_it->second; - } - - // No outgoing edge means termination - return END(); - } - - std::map> nodes_; - std::map edges_; - std::map conditional_edges_; - std::string entry_point_; -}; - -} // namespace graph -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/graph/graph_node.h b/third_party/gopher-orch/include/gopher/orch/graph/graph_node.h deleted file mode 100644 index 83851f9b..00000000 --- a/third_party/gopher-orch/include/gopher/orch/graph/graph_node.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -// GraphNode - A node in the state graph -// -// GraphNode wraps a processing function that transforms GraphState. -// It can be created from: -// - A synchronous lambda: (GraphState) -> GraphState -// - An async Runnable: JsonRunnablePtr -// -// All node execution is async through the dispatcher. - -#include -#include -#include - -#include "gopher/orch/core/config.h" -#include "gopher/orch/core/types.h" -#include "gopher/orch/graph/graph_state.h" - -namespace gopher { -namespace orch { -namespace graph { - -using namespace gopher::orch::core; - -// ============================================================================= -// GraphNode - A node in the state graph -// ============================================================================= - -class GraphNode { - public: - using NodeFunc = std::function; - - GraphNode(const std::string& name, NodeFunc func) - : name_(name), func_(std::move(func)) {} - - const std::string& name() const { return name_; } - - void invoke(const GraphState& state, - const RunnableConfig& config, - Dispatcher& dispatcher, - GraphStateCallback callback) { - func_(state, config, dispatcher, std::move(callback)); - } - - private: - std::string name_; - NodeFunc func_; -}; - -} // namespace graph -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/graph/graph_state.h b/third_party/gopher-orch/include/gopher/orch/graph/graph_state.h deleted file mode 100644 index edbfdecd..00000000 --- a/third_party/gopher-orch/include/gopher/orch/graph/graph_state.h +++ /dev/null @@ -1,277 +0,0 @@ -#pragma once - -// GraphState - State container for StateGraph workflows -// -// Design principles: -// - Channel-based state management with optional reducers -// - Version tracking for change detection -// - JSON serialization for persistence and debugging -// - Thread-safe for concurrent node execution in Pregel model - -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { -namespace graph { - -using namespace gopher::orch::core; - -// ============================================================================= -// StateChannel - Manages a single piece of state with optional reducer -// ============================================================================= -// -// Reducers enable accumulating results from multiple parallel nodes. -// Without a reducer, last-write-wins semantics apply. -// -// Example reducers: -// - Append reducer for messages: [](a, b) { return concat(a, b); } -// - Max reducer for scores: [](a, b) { return max(a, b); } -// - Merge reducer for objects: [](a, b) { return merge(a, b); } - -template -class StateChannel { - public: - using Reducer = std::function; - - // Default constructor: last-write-wins semantics - StateChannel() : has_value_(false), version_(0), reducer_(nullptr) {} - - // Constructor with reducer: values are combined using the reducer function - explicit StateChannel(Reducer reducer) - : has_value_(false), version_(0), reducer_(std::move(reducer)) {} - - // Apply an update to this channel - // If a reducer is set and we have a previous value, combine them - // Otherwise, just store the new value - void update(const T& new_value) { - if (reducer_ && has_value_) { - value_ = reducer_(value_, new_value); - } else { - value_ = new_value; - has_value_ = true; - } - version_++; - } - - // Get the current value - const T& value() const { return value_; } - - // Check if this channel has been set - bool hasValue() const { return has_value_; } - - // Get the version number (incremented on each update) - uint64_t version() const { return version_; } - - // Reset the channel to its initial state - void reset() { - value_ = T(); - has_value_ = false; - version_ = 0; - } - - private: - T value_; - bool has_value_; - uint64_t version_; - Reducer reducer_; -}; - -// ============================================================================= -// JsonReducer - Common reducers for JsonValue channels -// ============================================================================= - -namespace reducers { - -// Last-write-wins (default behavior) -inline JsonValue lastWriteWins(const JsonValue& /* old_value */, - const JsonValue& new_value) { - return new_value; -} - -// Append arrays: [1, 2] + [3, 4] = [1, 2, 3, 4] -inline JsonValue appendArray(const JsonValue& old_value, - const JsonValue& new_value) { - if (!old_value.isArray() || !new_value.isArray()) { - return new_value; - } - JsonValue result = JsonValue::array(); - for (size_t i = 0; i < old_value.size(); ++i) { - result.push_back(old_value[i]); - } - for (size_t i = 0; i < new_value.size(); ++i) { - result.push_back(new_value[i]); - } - return result; -} - -// Merge objects (shallow): {a: 1} + {b: 2} = {a: 1, b: 2} -inline JsonValue mergeObjects(const JsonValue& old_value, - const JsonValue& new_value) { - if (!old_value.isObject() || !new_value.isObject()) { - return new_value; - } - JsonValue result = old_value; - for (const auto& key : new_value.keys()) { - result[key] = new_value[key]; - } - return result; -} - -} // namespace reducers - -// ============================================================================= -// ChannelConfig - Configuration for a state channel -// ============================================================================= - -struct ChannelConfig { - using Reducer = std::function; - - // Optional reducer function for combining values - Reducer reducer; - - // Default value when channel is not set - JsonValue default_value; - - ChannelConfig() : reducer(nullptr), default_value(JsonValue::null()) {} - - explicit ChannelConfig(Reducer r) - : reducer(std::move(r)), default_value(JsonValue::null()) {} - - ChannelConfig(Reducer r, JsonValue def) - : reducer(std::move(r)), default_value(std::move(def)) {} -}; - -// ============================================================================= -// GraphState - Container for all state channels -// ============================================================================= -// -// GraphState holds all the data flowing through a StateGraph. -// Each key maps to a channel that can have an optional reducer. -// -// Lifecycle: -// 1. Create from input JSON -// 2. Nodes read state, produce updates -// 3. Updates are merged (using reducers if configured) -// 4. Final state is serialized to JSON - -class GraphState { - public: - using Reducer = std::function; - - GraphState() = default; - - // Configure a channel with a reducer - // Must be called before any updates to that channel - void configureChannel(const std::string& key, Reducer reducer) { - reducers_[key] = std::move(reducer); - } - - // Configure a channel with default value - void configureChannel(const std::string& key, - Reducer reducer, - const JsonValue& default_value) { - reducers_[key] = std::move(reducer); - channels_[key] = default_value; - versions_[key] = 0; - } - - // Set a value by key (applies reducer if configured) - void set(const std::string& key, const JsonValue& value) { - auto reducer_it = reducers_.find(key); - auto existing_it = channels_.find(key); - - if (reducer_it != reducers_.end() && existing_it != channels_.end() && - reducer_it->second) { - // Apply reducer to combine old and new values - channels_[key] = reducer_it->second(existing_it->second, value); - } else { - // Last-write-wins - channels_[key] = value; - } - versions_[key]++; - } - - // Get a value by key (returns null if not found) - JsonValue get(const std::string& key) const { - auto it = channels_.find(key); - if (it == channels_.end()) { - return JsonValue::null(); - } - return it->second; - } - - // Check if key exists - bool has(const std::string& key) const { - return channels_.find(key) != channels_.end(); - } - - // Get version of a key (0 if never set) - uint64_t version(const std::string& key) const { - auto it = versions_.find(key); - return it != versions_.end() ? it->second : 0; - } - - // Get all keys - std::vector keys() const { - std::vector result; - result.reserve(channels_.size()); - for (const auto& entry : channels_) { - result.push_back(entry.first); - } - return result; - } - - // Serialize to JSON - JsonValue toJson() const { - JsonValue result = JsonValue::object(); - for (const auto& entry : channels_) { - result[entry.first] = entry.second; - } - return result; - } - - // Deserialize from JSON - static GraphState fromJson(const JsonValue& json) { - GraphState state; - if (json.isObject()) { - for (const auto& key : json.keys()) { - state.channels_[key] = json[key]; - state.versions_[key] = 1; - } - } - return state; - } - - // Merge another state into this one (respects reducers) - void merge(const GraphState& other) { - for (const auto& entry : other.channels_) { - set(entry.first, entry.second); - } - } - - // Create a copy with the same reducer configuration - GraphState copy() const { - GraphState result; - result.channels_ = channels_; - result.versions_ = versions_; - result.reducers_ = reducers_; - return result; - } - - private: - std::map channels_; - std::map versions_; - std::map reducers_; -}; - -// Callback type for graph node completion -using GraphStateCallback = std::function)>; - -} // namespace graph -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/graph/state_graph.h b/third_party/gopher-orch/include/gopher/orch/graph/state_graph.h deleted file mode 100644 index 2f4d4e67..00000000 --- a/third_party/gopher-orch/include/gopher/orch/graph/state_graph.h +++ /dev/null @@ -1,187 +0,0 @@ -#pragma once - -// StateGraph - Stateful workflow graphs (LangGraph-inspired) -// -// Implements the Pregel model (Bulk Synchronous Parallel): -// 1. PLAN: Determine which nodes can execute -// 2. EXECUTE: Run scheduled nodes -// 3. UPDATE: Apply state changes atomically, prepare next step -// -// Usage: -// StateGraph graph; -// graph.addNode("start", [](const GraphState& s) { ... }) -// .addNode("process", processRunnable) -// .addEdge("start", "process") -// .addEdge("process", StateGraph::END()) -// .setEntryPoint("start"); -// auto compiled = graph.compile(); -// compiled->invoke(input, config, dispatcher, callback); - -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" -#include "gopher/orch/graph/compiled_graph.h" -#include "gopher/orch/graph/graph_node.h" -#include "gopher/orch/graph/graph_state.h" - -namespace gopher { -namespace orch { -namespace graph { - -using namespace gopher::orch::core; - -// ============================================================================= -// StateGraph - Builder for stateful workflow graphs -// ============================================================================= -// -// StateGraph provides a fluent API for building workflow graphs: -// - addNode(): Add processing nodes -// - addEdge(): Add direct transitions between nodes -// - addConditionalEdge(): Add conditional transitions based on state -// - setEntryPoint(): Define the starting node -// - compile(): Create an executable CompiledStateGraph -// -// The compiled graph implements Runnable, so it can -// be composed with Sequence, Parallel, Router, and resilience wrappers. - -class StateGraph { - public: - // Condition function that evaluates state and returns next node name - using EdgeCondition = std::function; - - // Special node name for graph termination - // Using static method for C++14 compatibility (inline variables are C++17) - static const std::string& END() { - static const std::string end_node = "__end__"; - return end_node; - } - - // Special node name for graph start (can be used in edges from START) - static const std::string& START() { - static const std::string start_node = "__start__"; - return start_node; - } - - StateGraph() = default; - - // ------------------------------------------------------------------------- - // Node Addition - // ------------------------------------------------------------------------- - - // Add a node with a JsonRunnable - // The runnable receives the full state as JSON and returns updates - StateGraph& addNode(const std::string& name, JsonRunnablePtr runnable) { - auto node_func = [runnable]( - const GraphState& state, const RunnableConfig& config, - Dispatcher& dispatcher, GraphStateCallback callback) { - runnable->invoke( - state.toJson(), config, dispatcher, - [state, callback = std::move(callback)](Result result) { - if (mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - return; - } - - // Merge runnable output into state - // Output keys overwrite existing state keys - GraphState new_state = state; - const auto& output = mcp::get(result); - if (output.isObject()) { - for (const auto& key : output.keys()) { - new_state.set(key, output[key]); - } - } - callback(makeSuccess(std::move(new_state))); - }); - }; - - nodes_[name] = std::make_shared(name, std::move(node_func)); - return *this; - } - - // Add a node with a synchronous lambda function - // The lambda receives current state and returns updated state - StateGraph& addNode(const std::string& name, - std::function func) { - auto node_func = [func](const GraphState& state, const RunnableConfig&, - Dispatcher& dispatcher, - GraphStateCallback callback) { - // Post to dispatcher to maintain async semantics - // This ensures callbacks are always invoked in dispatcher context - dispatcher.post([func, state, callback = std::move(callback)]() { - try { - GraphState result = func(state); - callback(makeSuccess(std::move(result))); - } catch (const std::exception& e) { - callback(makeOrchError( - OrchError::INTERNAL_ERROR, - std::string("Node execution error: ") + e.what())); - } - }); - }; - - nodes_[name] = std::make_shared(name, std::move(node_func)); - return *this; - } - - // Add a node with an async lambda function - // The lambda receives state and callback, must invoke callback exactly once - StateGraph& addNodeAsync(const std::string& name, GraphNode::NodeFunc func) { - nodes_[name] = std::make_shared(name, std::move(func)); - return *this; - } - - // ------------------------------------------------------------------------- - // Edge Addition - // ------------------------------------------------------------------------- - - // Add a direct edge (always transitions from -> to) - StateGraph& addEdge(const std::string& from, const std::string& to) { - edges_[from] = to; - return *this; - } - - // Add a conditional edge (transitions based on state evaluation) - // The condition function returns the name of the next node - StateGraph& addConditionalEdge(const std::string& from, - EdgeCondition condition) { - conditional_edges_[from] = std::move(condition); - return *this; - } - - // ------------------------------------------------------------------------- - // Graph Configuration - // ------------------------------------------------------------------------- - - // Set the entry point node (first node to execute) - StateGraph& setEntryPoint(const std::string& node) { - entry_point_ = node; - return *this; - } - - // ------------------------------------------------------------------------- - // Compilation - // ------------------------------------------------------------------------- - - // Compile the graph into an executable form - // Returns a CompiledStateGraph that implements Runnable - std::shared_ptr compile() { - return std::make_shared( - nodes_, edges_, conditional_edges_, entry_point_); - } - - private: - std::map> nodes_; - std::map edges_; - std::map conditional_edges_; - std::string entry_point_; - - friend class CompiledStateGraph; -}; - -} // namespace graph -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/human/approval.h b/third_party/gopher-orch/include/gopher/orch/human/approval.h deleted file mode 100644 index 48bfe102..00000000 --- a/third_party/gopher-orch/include/gopher/orch/human/approval.h +++ /dev/null @@ -1,464 +0,0 @@ -#pragma once - -// HumanApproval - Human-in-the-loop approval gate for Runnable operations -// -// This module provides a way to pause execution and request human approval -// before proceeding with sensitive or irreversible operations. -// -// The approval flow: -// 1. HumanApproval wraps an inner Runnable -// 2. When invoked, it creates an ApprovalRequest with preview and context -// 3. The ApprovalHandler is called to get human decision -// 4. If approved, the inner Runnable is invoked (possibly with modifications) -// 5. If denied, an error is returned -// -// Usage: -// auto handler = std::make_shared([](auto& req) { -// // Show UI, get decision... -// return ApprovalResponse{true, "Approved by user"}; -// }); -// -// auto protected_op = HumanApproval::create( -// dangerous_operation, -// handler, -// "This operation will modify production data. Continue?" -// ); - -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { -namespace human { - -// ============================================================================= -// ApprovalRequest - Information sent for human review -// ============================================================================= - -// ApprovalRequest contains all the context a human needs to make a decision. -// It includes: -// - action_name: What operation is being performed -// - preview: A preview of what will happen (input data, expected effects) -// - prompt: A human-readable question/message -// - metadata: Additional context (tags, source, urgency, etc.) -struct ApprovalRequest { - std::string action_name; // Name of the action requiring approval - core::JsonValue preview; // Preview of input/effects for review - std::string prompt; // Human-readable prompt/question - core::JsonValue metadata; // Additional context - - ApprovalRequest() - : preview(core::JsonValue::object()), - metadata(core::JsonValue::object()) {} -}; - -// ============================================================================= -// ApprovalResponse - Human decision -// ============================================================================= - -// ApprovalResponse contains the human's decision and any modifications. -// The modifications field allows the human to adjust the input before -// the operation proceeds (e.g., correcting parameters, reducing scope). -struct ApprovalResponse { - bool approved; // True if the operation should proceed - std::string reason; // Explanation for the decision - core::JsonValue modifications; // Optional modifications to input - - ApprovalResponse() : approved(false), modifications(core::JsonValue()) {} - - // Factory methods for common responses - static ApprovalResponse approve(const std::string& reason = "Approved") { - ApprovalResponse resp; - resp.approved = true; - resp.reason = reason; - return resp; - } - - static ApprovalResponse deny(const std::string& reason = "Denied") { - ApprovalResponse resp; - resp.approved = false; - resp.reason = reason; - return resp; - } - - static ApprovalResponse approveWithModifications( - const core::JsonValue& mods, - const std::string& reason = "Approved with modifications") { - ApprovalResponse resp; - resp.approved = true; - resp.reason = reason; - resp.modifications = mods; - return resp; - } -}; - -// ============================================================================= -// ApprovalHandler - Interface for requesting human approval -// ============================================================================= - -// ApprovalHandler is the interface for different approval mechanisms. -// Implementations might: -// - Show a CLI prompt -// - Display a GUI dialog -// - Send a notification and wait for response -// - Use an automated approval system (for testing) -// -// The callback-based API allows async approval (e.g., waiting for external -// response). -class ApprovalHandler { - public: - virtual ~ApprovalHandler() = default; - - // Request approval from a human. - // The callback must be invoked exactly once with the response. - // Implementations should ensure the callback is eventually called, - // even on timeout (with approved=false). - virtual void requestApproval( - const ApprovalRequest& request, - std::function callback) = 0; -}; - -// ============================================================================= -// HumanApproval - Wrap a runnable with human approval gate -// ============================================================================= - -// HumanApproval wraps an inner Runnable and gates it with human approval. -// The approval flow is: -// 1. Create ApprovalRequest with preview of the input -// 2. Call ApprovalHandler::requestApproval -// 3. On approval: invoke inner Runnable (with modifications if provided) -// 4. On denial: return error with reason -// -// Thread safety: The approval callback may be invoked on any thread. -// The inner Runnable invoke is always called on the dispatcher thread. -template -class HumanApproval : public core::Runnable { - public: - using Ptr = std::shared_ptr>; - using InnerPtr = typename core::Runnable::Ptr; - - HumanApproval(InnerPtr inner, - std::shared_ptr handler, - std::string prompt) - : inner_(std::move(inner)), - handler_(std::move(handler)), - prompt_(std::move(prompt)) {} - - std::string name() const override { - return "HumanApproval(" + inner_->name() + ")"; - } - - void invoke(const TInput& input, - const core::RunnableConfig& config, - core::Dispatcher& dispatcher, - core::ResultCallback callback) override { - // Build the approval request - ApprovalRequest request; - request.action_name = inner_->name(); - request.preview = toJsonPreview(input); - request.prompt = prompt_; - - // Capture what we need for the callback - // Use static_pointer_cast to get the correct type since we inherit - // enable_shared_from_this from Runnable base class - auto self = std::static_pointer_cast>( - this->shared_from_this()); - auto inner = inner_; - auto cfg = config; - - // Request approval (may be async) - handler_->requestApproval( - request, [self, inner, cfg, &dispatcher, callback, - input](ApprovalResponse response) mutable { - if (!response.approved) { - // Denied - post error to dispatcher - dispatcher.post([callback, response]() { - callback(core::Result(core::Error( - core::OrchError::APPROVAL_DENIED, response.reason))); - }); - return; - } - - // Approved - invoke inner runnable - // Apply modifications if provided - TInput final_input = input; - if (!response.modifications.isNull()) { - final_input = - self->fromJsonModifications(input, response.modifications); - } - - // Post invoke to dispatcher to ensure we're in the right context - dispatcher.post([inner, final_input, cfg, &dispatcher, callback]() { - inner->invoke(final_input, cfg, dispatcher, std::move(callback)); - }); - }); - } - - // Factory method - static Ptr create(InnerPtr inner, - std::shared_ptr handler, - const std::string& prompt) { - return std::make_shared>( - std::move(inner), std::move(handler), prompt); - } - - protected: - // Convert input to JSON for preview - // Default implementation works for JsonValue inputs - // Override this for custom preview formatting with non-JSON types - virtual core::JsonValue toJsonPreview(const TInput& input) { - return toJsonImpl(input); - } - - // Apply modifications to input - // Default implementation works for JsonValue inputs - // Override this for custom modification handling with non-JSON types - virtual TInput fromJsonModifications(const TInput& original, - const core::JsonValue& mods) { - (void)original; - return fromJsonImpl(mods); - } - - private: - // Type-specific JSON conversion helpers - // These use SFINAE to handle JsonValue vs other types - - // For JsonValue inputs, just return as-is - template - typename std::enable_if::value, - core::JsonValue>::type - toJsonImpl(const T& input) const { - return input; - } - - // For non-JsonValue inputs, attempt construction - template - typename std::enable_if::value, - core::JsonValue>::type - toJsonImpl(const T& input) const { - return core::JsonValue(input); - } - - // For JsonValue outputs, just return as-is - template - typename std::enable_if::value, T>::type - fromJsonImpl(const core::JsonValue& json) const { - return json; - } - - // For non-JsonValue outputs, this is a placeholder that will fail at compile - // time Users should override fromJsonModifications for non-JsonValue types - template - typename std::enable_if::value, T>::type - fromJsonImpl(const core::JsonValue& json) const { - // This static_assert provides a clear error message - static_assert(std::is_same::value, - "HumanApproval with non-JsonValue types requires " - "overriding fromJsonModifications()"); - (void)json; - return T{}; - } - - InnerPtr inner_; - std::shared_ptr handler_; - std::string prompt_; -}; - -// ============================================================================= -// CallbackApprovalHandler - Use a callback for approval -// ============================================================================= - -// CallbackApprovalHandler uses a synchronous callback function to make -// approval decisions. This is useful for: -// - Testing with deterministic approval logic -// - Simple CLI prompts -// - Automated approval based on rules -class CallbackApprovalHandler : public ApprovalHandler { - public: - // Callback type: takes request, returns response - using ApprovalCallback = - std::function; - - explicit CallbackApprovalHandler(ApprovalCallback callback) - : callback_(std::move(callback)) {} - - void requestApproval( - const ApprovalRequest& request, - std::function callback) override { - // Invoke the callback synchronously - ApprovalResponse response = callback_(request); - callback(std::move(response)); - } - - private: - ApprovalCallback callback_; -}; - -// ============================================================================= -// AsyncCallbackApprovalHandler - Use an async callback for approval -// ============================================================================= - -// AsyncCallbackApprovalHandler allows fully async approval decisions. -// The callback receives both the request and a response callback. -class AsyncCallbackApprovalHandler : public ApprovalHandler { - public: - using AsyncApprovalCallback = std::function)>; - - explicit AsyncCallbackApprovalHandler(AsyncApprovalCallback callback) - : callback_(std::move(callback)) {} - - void requestApproval( - const ApprovalRequest& request, - std::function callback) override { - callback_(request, std::move(callback)); - } - - private: - AsyncApprovalCallback callback_; -}; - -// ============================================================================= -// AutoApprovalHandler - Automatically approves (for testing) -// ============================================================================= - -// AutoApprovalHandler automatically approves all requests. -// Use this for: -// - Unit testing the approval flow -// - Development/staging environments -// - Non-sensitive operations that still need the approval interface -class AutoApprovalHandler : public ApprovalHandler { - public: - explicit AutoApprovalHandler(const std::string& reason = "Auto-approved") - : reason_(reason) {} - - void requestApproval( - const ApprovalRequest& request, - std::function callback) override { - (void)request; - callback(ApprovalResponse::approve(reason_)); - } - - private: - std::string reason_; -}; - -// ============================================================================= -// AutoDenyHandler - Automatically denies (for testing) -// ============================================================================= - -// AutoDenyHandler automatically denies all requests. -// Use this for: -// - Testing error handling paths -// - Temporarily disabling operations -// - Safety fallback when approval system is unavailable -class AutoDenyHandler : public ApprovalHandler { - public: - explicit AutoDenyHandler(const std::string& reason = "Auto-denied") - : reason_(reason) {} - - void requestApproval( - const ApprovalRequest& request, - std::function callback) override { - (void)request; - callback(ApprovalResponse::deny(reason_)); - } - - private: - std::string reason_; -}; - -// ============================================================================= -// ConditionalApprovalHandler - Approve based on condition -// ============================================================================= - -// ConditionalApprovalHandler approves or denies based on a predicate. -// Useful for rule-based automatic approval of certain operations. -class ConditionalApprovalHandler : public ApprovalHandler { - public: - using Predicate = std::function; - - explicit ConditionalApprovalHandler( - Predicate predicate, - const std::string& approve_reason = "Condition met", - const std::string& deny_reason = "Condition not met") - : predicate_(std::move(predicate)), - approve_reason_(approve_reason), - deny_reason_(deny_reason) {} - - void requestApproval( - const ApprovalRequest& request, - std::function callback) override { - if (predicate_(request)) { - callback(ApprovalResponse::approve(approve_reason_)); - } else { - callback(ApprovalResponse::deny(deny_reason_)); - } - } - - private: - Predicate predicate_; - std::string approve_reason_; - std::string deny_reason_; -}; - -// ============================================================================= -// RecordingApprovalHandler - Records requests for testing -// ============================================================================= - -// RecordingApprovalHandler records all requests and delegates to an inner -// handler. Useful for testing that the right requests are being made. -class RecordingApprovalHandler : public ApprovalHandler { - public: - explicit RecordingApprovalHandler(std::shared_ptr inner) - : inner_(std::move(inner)) {} - - void requestApproval( - const ApprovalRequest& request, - std::function callback) override { - { - std::lock_guard lock(mutex_); - recorded_requests_.push_back(request); - } - inner_->requestApproval(request, std::move(callback)); - } - - // Get all recorded requests - std::vector recordedRequests() const { - std::lock_guard lock(mutex_); - return recorded_requests_; - } - - // Get the number of recorded requests - size_t requestCount() const { - std::lock_guard lock(mutex_); - return recorded_requests_.size(); - } - - // Clear recorded requests - void clearRecords() { - std::lock_guard lock(mutex_); - recorded_requests_.clear(); - } - - private: - std::shared_ptr inner_; - mutable std::mutex mutex_; - std::vector recorded_requests_; -}; - -// ============================================================================= -// Convenience type aliases -// ============================================================================= - -// JSON-to-JSON human approval wrapper -using JsonHumanApproval = HumanApproval; - -} // namespace human -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/llm/anthropic_provider.h b/third_party/gopher-orch/include/gopher/orch/llm/anthropic_provider.h deleted file mode 100644 index c538bead..00000000 --- a/third_party/gopher-orch/include/gopher/orch/llm/anthropic_provider.h +++ /dev/null @@ -1,139 +0,0 @@ -#pragma once - -// AnthropicProvider - Anthropic API implementation of LLMProvider -// -// Supports Anthropic's Messages API including tool use. -// Compatible with Claude models (claude-3-opus, claude-3-sonnet, -// claude-3-haiku, etc.) -// -// Usage: -// auto provider = AnthropicProvider::create("sk-ant-..."); -// -// LLMConfig config("claude-3-5-sonnet-latest"); -// provider->chat(messages, tools, config, dispatcher, callback); - -#include -#include -#include - -#include "gopher/orch/llm/llm_provider.h" - -namespace gopher { -namespace orch { -namespace llm { - -// Forward declaration -class AnthropicProvider; -using AnthropicProviderPtr = std::shared_ptr; - -// Anthropic-specific configuration -struct AnthropicConfig { - std::string api_key; - std::string base_url = "https://api.anthropic.com"; - std::string api_version = "2023-06-01"; - - // Beta features - bool enable_computer_use = false; - std::vector betas; // Beta feature flags - - AnthropicConfig() = default; - explicit AnthropicConfig(const std::string& key) : api_key(key) {} - - AnthropicConfig& withBaseUrl(const std::string& url) { - base_url = url; - return *this; - } - - AnthropicConfig& withApiVersion(const std::string& version) { - api_version = version; - return *this; - } - - AnthropicConfig& withBeta(const std::string& beta) { - betas.push_back(beta); - return *this; - } - - AnthropicConfig& withComputerUse(bool enable = true) { - enable_computer_use = enable; - if (enable) { - betas.push_back("computer-use-2024-10-22"); - } - return *this; - } -}; - -// AnthropicProvider - Anthropic API implementation -// -// Supported models: -// - claude-3-5-sonnet-latest, claude-3-5-sonnet-20241022 -// - claude-3-5-haiku-latest, claude-3-5-haiku-20241022 -// - claude-3-opus-20240229 -// - claude-3-sonnet-20240229 -// - claude-3-haiku-20240307 -// -// Thread Safety: -// - Thread-safe after construction -// - All callbacks invoked in dispatcher context -class AnthropicProvider : public LLMProvider { - public: - using Ptr = std::shared_ptr; - - // Factory methods - static Ptr create(const std::string& api_key); - static Ptr create(const std::string& api_key, const std::string& base_url); - static Ptr create(const AnthropicConfig& config); - - ~AnthropicProvider() override; - - // LLMProvider interface - std::string name() const override { return "anthropic"; } - - void chat(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) override; - - bool supportsStreaming() const override { return true; } - - void chatStream(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - StreamCallback on_chunk, - ChatCallback on_complete) override; - - bool isModelSupported(const std::string& model) const override; - std::vector supportedModels() const override; - - std::string endpoint() const override; - bool isConfigured() const override; - - private: - explicit AnthropicProvider(const AnthropicConfig& config); - - // Build request JSON (Anthropic format) - JsonValue buildRequest(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - bool stream = false) const; - - // Parse response JSON - Result parseResponse(const JsonValue& response) const; - - // Convert Message to Anthropic format - // Note: Anthropic separates system from messages - std::pair messagesToAnthropicFormat( - const std::vector& messages) const; - - // Convert ToolSpec to Anthropic tool format - JsonValue toolToJson(const ToolSpec& tool) const; - - class Impl; - std::unique_ptr impl_; -}; - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/llm/llm.h b/third_party/gopher-orch/include/gopher/orch/llm/llm.h deleted file mode 100644 index 10de6dd2..00000000 --- a/third_party/gopher-orch/include/gopher/orch/llm/llm.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -// LLM Module - Unified interface for LLM providers -// -// This module provides: -// - LLMProvider: Abstract interface for LLM API calls -// - OpenAIProvider: OpenAI API (GPT-4, etc.) -// - AnthropicProvider: Anthropic API (Claude models) -// - Common types: Message, ToolCall, LLMResponse, etc. -// -// Usage: -// #include "gopher/orch/llm/llm.h" -// using namespace gopher::orch::llm; -// -// auto provider = createOpenAIProvider("sk-..."); -// LLMConfig config("gpt-4o"); -// config.withTemperature(0.7); -// -// std::vector messages = { -// Message::system("You are a helpful assistant."), -// Message::user("Hello!") -// }; -// -// provider->chat(messages, {}, config, dispatcher, [](Result r) -// { -// if (r.isOk()) { -// std::cout << r.value().message.content << std::endl; -// } -// }); - -// Core types -#include "gopher/orch/llm/llm_types.h" - -// Base provider interface -#include "gopher/orch/llm/llm_provider.h" - -// Provider implementations -#include "gopher/orch/llm/anthropic_provider.h" -#include "gopher/orch/llm/openai_provider.h" - -namespace gopher { -namespace orch { -namespace llm { - -// Convenience re-exports at llm namespace level - -// Types -using core::Dispatcher; -using core::Error; -using core::JsonValue; -using core::Result; - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/llm/llm_provider.h b/third_party/gopher-orch/include/gopher/orch/llm/llm_provider.h deleted file mode 100644 index d322c57a..00000000 --- a/third_party/gopher-orch/include/gopher/orch/llm/llm_provider.h +++ /dev/null @@ -1,191 +0,0 @@ -#pragma once - -// LLMProvider - Abstract interface for LLM providers -// -// Provides a unified async interface for interacting with various LLM providers -// (OpenAI, Anthropic, Ollama, etc.). Each provider implements this interface -// to handle provider-specific API details. -// -// Usage: -// auto provider = OpenAIProvider::create(api_key); -// LLMConfig config("gpt-4"); -// config.withTemperature(0.7); -// -// provider->chat(messages, tools, config, dispatcher, [](Result -// r) { -// if (r.isOk()) { -// auto response = r.value(); -// // Handle response... -// } -// }); - -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" -#include "gopher/orch/llm/llm_types.h" - -namespace gopher { -namespace orch { -namespace llm { - -using namespace gopher::orch::core; - -// Forward declarations -class LLMProvider; -using LLMProviderPtr = std::shared_ptr; - -// Callback types -using ChatCallback = std::function)>; -using StreamCallback = std::function; - -// LLMProvider - Abstract base class for LLM providers -// -// Thread Safety: -// - All public methods must be called from dispatcher thread -// - Callbacks are invoked in dispatcher thread context -// -// Implementations: -// - OpenAIProvider: OpenAI API (GPT-4, GPT-3.5, etc.) -// - AnthropicProvider: Anthropic API (Claude models) -// - OllamaProvider: Local Ollama server -class LLMProvider { - public: - using Ptr = std::shared_ptr; - - virtual ~LLMProvider() = default; - - // Provider identification - virtual std::string name() const = 0; - - // ═══════════════════════════════════════════════════════════════════════════ - // CHAT COMPLETION - // ═══════════════════════════════════════════════════════════════════════════ - - // Send a chat completion request - // - // Parameters: - // messages - Conversation history - // tools - Available tools (empty if no tools) - // config - Model configuration (model, temperature, etc.) - // dispatcher - Event dispatcher for async callback - // callback - Called with response or error - // - // The callback receives: - // - LLMResponse on success (may contain tool_calls if LLM wants to use - // tools) - // - Error on failure (network, auth, rate limit, etc.) - virtual void chat(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) = 0; - - // Convenience overload without tools - void chat(const std::vector& messages, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) { - chat(messages, {}, config, dispatcher, std::move(callback)); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // STREAMING (Optional) - // ═══════════════════════════════════════════════════════════════════════════ - - // Check if provider supports streaming - virtual bool supportsStreaming() const { return false; } - - // Stream a chat completion request - // - // Parameters: - // messages - Conversation history - // tools - Available tools - // config - Model configuration - // dispatcher - Event dispatcher - // on_chunk - Called for each chunk received - // on_complete - Called when stream completes or errors - // - // Default implementation falls back to non-streaming chat - virtual void chatStream(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - StreamCallback on_chunk, - ChatCallback on_complete) { - // Default: fall back to non-streaming - chat(messages, tools, config, dispatcher, std::move(on_complete)); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // VALIDATION - // ═══════════════════════════════════════════════════════════════════════════ - - // Check if a model is supported by this provider - virtual bool isModelSupported(const std::string& model) const = 0; - - // Get list of supported models (may be empty if dynamic) - virtual std::vector supportedModels() const { return {}; } - - // ═══════════════════════════════════════════════════════════════════════════ - // CONFIGURATION - // ═══════════════════════════════════════════════════════════════════════════ - - // Get current API endpoint (for debugging/logging) - virtual std::string endpoint() const = 0; - - // Check if provider is properly configured (has API key, etc.) - virtual bool isConfigured() const = 0; -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// PROVIDER FACTORY -// ═══════════════════════════════════════════════════════════════════════════ - -// Provider types for factory -enum class ProviderType { OPENAI, ANTHROPIC, OLLAMA, CUSTOM }; - -// Provider configuration -struct ProviderConfig { - ProviderType type = ProviderType::OPENAI; - std::string api_key; - std::string base_url; // Override default endpoint - std::map headers; // Additional headers - - ProviderConfig() = default; - explicit ProviderConfig(ProviderType t) : type(t) {} - - ProviderConfig& withApiKey(const std::string& key) { - api_key = key; - return *this; - } - - ProviderConfig& withBaseUrl(const std::string& url) { - base_url = url; - return *this; - } - - ProviderConfig& withHeader(const std::string& name, - const std::string& value) { - headers[name] = value; - return *this; - } -}; - -// Factory function to create providers -// Implemented in llm_factory.cpp -LLMProviderPtr createProvider(const ProviderConfig& config); - -// Convenience factory functions -LLMProviderPtr createOpenAIProvider(const std::string& api_key, - const std::string& base_url = ""); -LLMProviderPtr createAnthropicProvider(const std::string& api_key, - const std::string& base_url = ""); -LLMProviderPtr createOllamaProvider( - const std::string& base_url = "http://localhost:11434"); - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/llm/llm_runnable.h b/third_party/gopher-orch/include/gopher/orch/llm/llm_runnable.h deleted file mode 100644 index b7e8e82a..00000000 --- a/third_party/gopher-orch/include/gopher/orch/llm/llm_runnable.h +++ /dev/null @@ -1,120 +0,0 @@ -#pragma once - -// LLMRunnable - Wraps LLMProvider as a composable Runnable -// -// Enables LLM calls to be composed with other Runnables in pipelines, -// sequences, and graphs. Transforms JSON input into LLM chat requests -// and returns LLM responses as JSON. -// -// Usage: -// auto provider = createOpenAIProvider("sk-..."); -// auto llm = LLMRunnable::create(provider, LLMConfig("gpt-4")); -// -// JsonValue input = JsonValue::object(); -// input["messages"] = messages_array; -// -// llm->invoke(input, config, dispatcher, [](Result result) { -// // Handle result... -// }); - -#include -#include - -#include "gopher/orch/core/runnable.h" -#include "gopher/orch/llm/llm_provider.h" -#include "gopher/orch/llm/llm_types.h" - -namespace gopher { -namespace orch { -namespace llm { - -using namespace gopher::orch::core; - -// LLMRunnable - Adapter that makes LLMProvider a Runnable -// -// Input Schema: -// { -// "messages": [ -// {"role": "system", "content": "..."}, -// {"role": "user", "content": "..."} -// ], -// "tools": [...], // optional -// "config": {...} // optional, overrides default config -// } -// -// Alternative: Simple string input becomes a user message -// "Hello, how are you?" -// -// Output Schema: -// { -// "message": { -// "role": "assistant", -// "content": "...", -// "tool_calls": [...] // optional -// }, -// "finish_reason": "stop" | "tool_calls" | "length", -// "usage": { -// "prompt_tokens": 50, -// "completion_tokens": 20, -// "total_tokens": 70 -// } -// } -class LLMRunnable : public Runnable { - public: - using Ptr = std::shared_ptr; - - // Factory method - static Ptr create(LLMProviderPtr provider, - const LLMConfig& config = LLMConfig()); - - // Runnable interface - std::string name() const override; - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override; - - // Accessors - LLMProviderPtr provider() const { return provider_; } - const LLMConfig& defaultConfig() const { return default_config_; } - - // Set default config - void setDefaultConfig(const LLMConfig& config) { default_config_ = config; } - - private: - LLMRunnable(LLMProviderPtr provider, const LLMConfig& config); - - // Parse input JSON into messages, tools, and config - struct ParsedInput { - std::vector messages; - std::vector tools; - LLMConfig config; - }; - ParsedInput parseInput(const JsonValue& input) const; - - // Convert LLMResponse to JSON output - static JsonValue responseToJson(const LLMResponse& response); - - // Convert Message to JSON - static JsonValue messageToJson(const Message& message); - - // Parse Message from JSON - static Message parseMessage(const JsonValue& json); - - // Parse ToolSpec from JSON - static ToolSpec parseToolSpec(const JsonValue& json); - - LLMProviderPtr provider_; - LLMConfig default_config_; -}; - -// Convenience factory function -inline LLMRunnable::Ptr makeLLMRunnable(LLMProviderPtr provider, - const LLMConfig& config = LLMConfig()) { - return LLMRunnable::create(std::move(provider), config); -} - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/llm/llm_types.h b/third_party/gopher-orch/include/gopher/orch/llm/llm_types.h deleted file mode 100644 index 535eec6b..00000000 --- a/third_party/gopher-orch/include/gopher/orch/llm/llm_types.h +++ /dev/null @@ -1,279 +0,0 @@ -#pragma once - -// LLM Types - Core types for LLM provider integration -// -// Provides message types, tool call structures, and response types -// for interacting with LLM providers (OpenAI, Anthropic, Ollama, etc.) - -#include -#include -#include - -#include "gopher/orch/core/types.h" - -namespace gopher { -namespace orch { -namespace llm { - -using namespace gopher::orch::core; - -// ═══════════════════════════════════════════════════════════════════════════ -// MESSAGE TYPES -// ═══════════════════════════════════════════════════════════════════════════ - -// Forward declaration -struct ToolCall; - -// Message role in conversation -enum class Role { - SYSTEM, // System prompt - USER, // User message - ASSISTANT, // Assistant response - TOOL // Tool result -}; - -// Convert Role to string -inline std::string roleToString(Role role) { - switch (role) { - case Role::SYSTEM: - return "system"; - case Role::USER: - return "user"; - case Role::ASSISTANT: - return "assistant"; - case Role::TOOL: - return "tool"; - default: - return "user"; - } -} - -// Parse string to Role -inline Role parseRole(const std::string& role) { - if (role == "system") - return Role::SYSTEM; - if (role == "user") - return Role::USER; - if (role == "assistant") - return Role::ASSISTANT; - if (role == "tool") - return Role::TOOL; - return Role::USER; -} - -// Tool call requested by LLM -struct ToolCall { - std::string id; // Unique ID for this call (used for matching results) - std::string name; // Tool name to call - JsonValue arguments; // Arguments as JSON - - ToolCall() = default; - ToolCall(const std::string& id_, - const std::string& name_, - const JsonValue& args_) - : id(id_), name(name_), arguments(args_) {} -}; - -// Message in conversation -struct Message { - Role role; - std::string content; - - // For tool responses (role = TOOL) - optional tool_call_id; - - // For assistant messages with tool calls - optional> tool_calls; - - Message() : role(Role::USER) {} - - Message(Role r, const std::string& c) - : role(r), content(c), tool_call_id(nullopt), tool_calls(nullopt) {} - - // Factory methods for convenience - static Message system(const std::string& content) { - return Message(Role::SYSTEM, content); - } - - static Message user(const std::string& content) { - return Message(Role::USER, content); - } - - static Message assistant(const std::string& content) { - Message msg(Role::ASSISTANT, content); - return msg; - } - - static Message assistantWithToolCalls(const std::vector& calls) { - Message msg(Role::ASSISTANT, ""); - msg.tool_calls = calls; - return msg; - } - - static Message toolResult(const std::string& call_id, - const std::string& content) { - Message msg(Role::TOOL, content); - msg.tool_call_id = call_id; - return msg; - } - - // Check if message has tool calls - bool hasToolCalls() const { - return tool_calls.has_value() && !tool_calls->empty(); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// TOOL SPECIFICATION (For telling LLM what tools are available) -// ═══════════════════════════════════════════════════════════════════════════ - -struct ToolSpec { - std::string name; - std::string description; - JsonValue parameters; // JSON Schema for parameters - - ToolSpec() = default; - ToolSpec(const std::string& n, const std::string& d, const JsonValue& p) - : name(n), description(d), parameters(p) {} -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// LLM CONFIGURATION -// ═══════════════════════════════════════════════════════════════════════════ - -struct LLMConfig { - std::string model; // e.g., "gpt-4", "claude-3-opus-20240229" - - optional temperature; // 0.0 - 2.0 - optional max_tokens; // Max response tokens - optional top_p; // Nucleus sampling - optional seed; // For reproducibility - - optional> stop; // Stop sequences - - // Request timeout - std::chrono::milliseconds timeout{60000}; - - LLMConfig() = default; - explicit LLMConfig(const std::string& m) : model(m) {} - - // Builder pattern - LLMConfig& withModel(const std::string& m) { - model = m; - return *this; - } - - LLMConfig& withTemperature(double t) { - temperature = t; - return *this; - } - - LLMConfig& withMaxTokens(int t) { - max_tokens = t; - return *this; - } - - LLMConfig& withTopP(double p) { - top_p = p; - return *this; - } - - LLMConfig& withSeed(int s) { - seed = s; - return *this; - } - - LLMConfig& withStop(const std::vector& s) { - stop = s; - return *this; - } - - LLMConfig& withTimeout(std::chrono::milliseconds t) { - timeout = t; - return *this; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// USAGE STATISTICS -// ═══════════════════════════════════════════════════════════════════════════ - -struct Usage { - int prompt_tokens = 0; - int completion_tokens = 0; - int total_tokens = 0; - - Usage() = default; - Usage(int prompt, int completion) - : prompt_tokens(prompt), - completion_tokens(completion), - total_tokens(prompt + completion) {} -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// LLM RESPONSE -// ═══════════════════════════════════════════════════════════════════════════ - -struct LLMResponse { - Message message; // The response message - std::string - finish_reason; // "stop", "tool_calls", "length", "content_filter" - optional usage; - - LLMResponse() = default; - - // Check if LLM wants to call tools - bool hasToolCalls() const { return message.hasToolCalls(); } - - // Get tool calls (empty vector if none) - const std::vector& toolCalls() const { - static const std::vector empty; - return message.tool_calls.has_value() ? *message.tool_calls : empty; - } - - // Check if conversation is complete (no more tool calls needed) - bool isComplete() const { - return finish_reason == "stop" || finish_reason == "end_turn"; - } - - // Check if response was truncated due to token limit - bool isTruncated() const { return finish_reason == "length"; } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// STREAMING TYPES (Optional, for streaming support) -// ═══════════════════════════════════════════════════════════════════════════ - -struct StreamDelta { - optional content; // Content chunk - optional tool_call; // Tool call chunk (partial) - optional finish_reason; -}; - -struct StreamChunk { - StreamDelta delta; - bool is_final = false; -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// ERROR CODES -// ═══════════════════════════════════════════════════════════════════════════ - -namespace LLMError { -enum : int { - OK = 0, - INVALID_API_KEY = -100, - RATE_LIMITED = -101, - CONTEXT_LENGTH_EXCEEDED = -102, - INVALID_MODEL = -103, - CONTENT_FILTERED = -104, - SERVICE_UNAVAILABLE = -105, - NETWORK_ERROR = -106, - PARSE_ERROR = -107, - UNKNOWN = -199 -}; -} // namespace LLMError - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/llm/openai_provider.h b/third_party/gopher-orch/include/gopher/orch/llm/openai_provider.h deleted file mode 100644 index 70ef3449..00000000 --- a/third_party/gopher-orch/include/gopher/orch/llm/openai_provider.h +++ /dev/null @@ -1,143 +0,0 @@ -#pragma once - -// OpenAIProvider - OpenAI API implementation of LLMProvider -// -// Supports OpenAI's chat completion API including function/tool calling. -// Compatible with OpenAI API and OpenAI-compatible endpoints (Azure, etc.) -// -// Usage: -// auto provider = OpenAIProvider::create("sk-..."); -// // Or with custom endpoint: -// auto provider = OpenAIProvider::create("sk-...", -// "https://custom.endpoint.com/v1"); -// -// LLMConfig config("gpt-4"); -// provider->chat(messages, tools, config, dispatcher, callback); - -#include -#include -#include - -#include "gopher/orch/llm/llm_provider.h" - -namespace gopher { -namespace orch { -namespace llm { - -// Forward declaration -class OpenAIProvider; -using OpenAIProviderPtr = std::shared_ptr; - -// OpenAI-specific configuration -struct OpenAIConfig { - std::string api_key; - std::string base_url = "https://api.openai.com/v1"; - std::string organization; // Optional org ID - - // Azure OpenAI specific - bool is_azure = false; - std::string azure_api_version = "2024-02-15-preview"; - std::string azure_deployment; // Deployment name for Azure - - OpenAIConfig() = default; - explicit OpenAIConfig(const std::string& key) : api_key(key) {} - - OpenAIConfig& withBaseUrl(const std::string& url) { - base_url = url; - return *this; - } - - OpenAIConfig& withOrganization(const std::string& org) { - organization = org; - return *this; - } - - OpenAIConfig& forAzure( - const std::string& deployment, - const std::string& api_version = "2024-02-15-preview") { - is_azure = true; - azure_deployment = deployment; - azure_api_version = api_version; - return *this; - } -}; - -// OpenAIProvider - OpenAI API implementation -// -// Supported models: -// - gpt-4, gpt-4-turbo, gpt-4o, gpt-4o-mini -// - gpt-3.5-turbo -// - o1, o1-mini, o1-preview (reasoning models) -// -// Thread Safety: -// - Thread-safe after construction -// - All callbacks invoked in dispatcher context -class OpenAIProvider : public LLMProvider { - public: - using Ptr = std::shared_ptr; - - // Factory methods - static Ptr create(const std::string& api_key); - static Ptr create(const std::string& api_key, const std::string& base_url); - static Ptr create(const OpenAIConfig& config); - - ~OpenAIProvider() override; - - // LLMProvider interface - std::string name() const override { return "openai"; } - - void chat(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) override; - - bool supportsStreaming() const override { return true; } - - void chatStream(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - StreamCallback on_chunk, - ChatCallback on_complete) override; - - bool isModelSupported(const std::string& model) const override; - std::vector supportedModels() const override; - - std::string endpoint() const override; - bool isConfigured() const override; - - // OpenAI-specific methods - - // Get/set organization ID - std::string organization() const; - void setOrganization(const std::string& org); - - private: - explicit OpenAIProvider(const OpenAIConfig& config); - - // Build request JSON - JsonValue buildRequest(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - bool stream = false) const; - - // Parse response JSON - Result parseResponse(const JsonValue& response) const; - - // Parse streaming chunk - Result parseStreamChunk(const std::string& data) const; - - // Convert Message to OpenAI format - JsonValue messageToJson(const Message& msg) const; - - // Convert ToolSpec to OpenAI function format - JsonValue toolToJson(const ToolSpec& tool) const; - - class Impl; - std::unique_ptr impl_; -}; - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/orch.h b/third_party/gopher-orch/include/gopher/orch/orch.h deleted file mode 100644 index 45deb66d..00000000 --- a/third_party/gopher-orch/include/gopher/orch/orch.h +++ /dev/null @@ -1,263 +0,0 @@ -#pragma once - -// gopher-orch - MCP Server Orchestration Framework -// -// Provides composable building blocks for agentic workflows: -// - Runnable: Universal async operation interface -// - Sequence, Parallel, Router: Composition patterns -// - StateGraph: Stateful workflow graphs (Pregel model) -// - StateMachine: Entity lifecycle management (FSM) -// - Server: Protocol-agnostic server abstraction -// - Resilience: Retry, Timeout, Fallback, CircuitBreaker -// -// Design principles: -// - Async-first with dispatcher-based callbacks -// - Type-safe with C++14 compatibility -// - Protocol-agnostic (MCP, REST, mock) -// - Explicit - no hidden magic - -// Core types and utilities -#include "gopher/orch/core/config.h" -#include "gopher/orch/core/lambda.h" -#include "gopher/orch/core/runnable.h" -#include "gopher/orch/core/types.h" - -// Composition patterns -#include "gopher/orch/composition/parallel.h" -#include "gopher/orch/composition/router.h" -#include "gopher/orch/composition/sequence.h" - -// Resilience patterns -#include "gopher/orch/resilience/circuit_breaker.h" -#include "gopher/orch/resilience/fallback.h" -#include "gopher/orch/resilience/retry.h" -#include "gopher/orch/resilience/timeout.h" - -// Graph patterns -#include "gopher/orch/graph/state_graph.h" - -// Finite State Machine -#include "gopher/orch/fsm/state_machine.h" - -// Callback system (Observability) -#include "gopher/orch/callback/callback_handler.h" -#include "gopher/orch/callback/callback_manager.h" - -// Human-in-the-Loop -#include "gopher/orch/human/approval.h" - -// LLM Providers -#include "gopher/orch/llm/llm.h" - -// Agent Framework -#include "gopher/orch/agent/agent_module.h" - -// Server abstraction -#include "gopher/orch/server/mock_server.h" -#include "gopher/orch/server/server.h" -#include "gopher/orch/server/server_composite.h" - -// MCP Server and REST Server (require gopher-mcp dependency) -// Conditionally included to avoid hard dependency -#ifdef GOPHER_ORCH_WITH_MCP -#include "gopher/orch/server/mcp_server.h" -#include "gopher/orch/server/rest_server.h" -#endif - -// FFI Layer - C API for cross-language bindings -// The C API headers are always available. The bridge header is internal. -// Use GOPHER_ORCH_WITH_FFI to include RAII C++ wrapper utilities. -#include "gopher/orch/ffi/orch_ffi.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#ifdef GOPHER_ORCH_WITH_FFI -#include "gopher/orch/ffi/orch_ffi_raii.h" -#endif - -// Convenience namespace imports -namespace gopher { -namespace orch { - -// Re-export core types at orch level -using core::Dispatcher; -using core::Error; -using core::JsonCallback; -using core::JsonRunnable; -using core::JsonRunnablePtr; -using core::JsonValue; -using core::Lambda; -using core::makeJsonLambda; -using core::makeLambda; -using core::makeLambdaAsync; -using core::makeOrchError; -using core::makeSuccess; -using core::nullopt; -using core::optional; -namespace OrchError = core::OrchError; // Namespace alias -using core::Result; -using core::ResultCallback; -using core::Runnable; -using core::RunnableConfig; - -// Re-export composition patterns -using composition::Parallel; -using composition::parallel; -using composition::ParallelBuilder; -using composition::Router; -using composition::router; -using composition::RouterBuilder; -using composition::Sequence; -using composition::sequence; -using composition::Sequence2; -using composition::SequenceBuilder; - -// Re-export resilience patterns -using resilience::CircuitBreaker; -using resilience::CircuitBreakerPolicy; -using resilience::CircuitState; -using resilience::Fallback; -using resilience::FallbackBuilder; -using resilience::JsonCircuitBreaker; -using resilience::JsonFallback; -using resilience::JsonRetry; -using resilience::JsonTimeout; -using resilience::Retry; -using resilience::RetryPolicy; -using resilience::Timeout; -using resilience::withCircuitBreaker; -using resilience::withFallback; -using resilience::withRetry; -using resilience::withTimeout; - -// Re-export graph patterns -using graph::ChannelConfig; -using graph::CompiledStateGraph; -using graph::GraphNode; -using graph::GraphState; -using graph::GraphStateCallback; -using graph::StateChannel; -using graph::StateGraph; -namespace reducers = graph::reducers; // Namespace alias for reducers - -// Re-export FSM components -using fsm::makeStateMachine; -using fsm::StateMachine; -using fsm::StateMachineBuilder; - -// Re-export callback system components -using callback::CallbackHandler; -using callback::CallbackManager; -using callback::ChainGuard; -using callback::EventType; -using callback::LoggingCallbackHandler; -using callback::NoOpCallbackHandler; -using callback::RunInfo; -using callback::ToolGuard; - -// Re-export human-in-the-loop components -using human::ApprovalHandler; -using human::ApprovalRequest; -using human::ApprovalResponse; -using human::AsyncCallbackApprovalHandler; -using human::AutoApprovalHandler; -using human::AutoDenyHandler; -using human::CallbackApprovalHandler; -using human::ConditionalApprovalHandler; -using human::HumanApproval; -using human::JsonHumanApproval; -using human::RecordingApprovalHandler; - -// Re-export server components -using server::ConnectionCallback; -using server::ConnectionState; -using server::makeMockServer; -using server::MockServer; -using server::Server; -using server::ServerComposite; -using server::ServerCompositePtr; -using server::ServerPtr; -using server::ServerTool; -using server::ServerToolInfo; -using server::ServerToolListCallback; -using server::ServerToolPtr; -using server::ToolMapping; - -// MCP Server and REST Server exports (conditional) -#ifdef GOPHER_ORCH_WITH_MCP -using server::HttpClient; -using server::HttpMethod; -using server::HttpResponse; -using server::makeRESTServer; -using server::MCPServer; -using server::MCPServerConfig; -using server::MCPServerPtr; -using server::RESTServer; -using server::RESTServerConfig; -using server::RESTServerPtr; -#endif - -// Re-export LLM components -using llm::AnthropicConfig; -using llm::AnthropicProvider; -using llm::ChatCallback; -using llm::createAnthropicProvider; -using llm::createOpenAIProvider; -using llm::createProvider; -using llm::LLMConfig; -using llm::LLMProvider; -using llm::LLMProviderPtr; -using llm::LLMResponse; -using llm::Message; -using llm::OpenAIConfig; -using llm::OpenAIProvider; -using llm::ProviderConfig; -using llm::ProviderType; -using llm::Role; -using llm::StreamCallback; -using llm::StreamChunk; -using llm::StreamDelta; -using llm::ToolCall; -using llm::ToolSpec; -using llm::Usage; -namespace LLMError = llm::LLMError; // Namespace alias for error codes - -// Re-export Agent components -using agent::Agent; -using agent::AgentCallback; -using agent::AgentConfig; -using agent::AgentPtr; -using agent::AgentResult; -using agent::AgentState; -using agent::AgentStatus; -using agent::AgentStep; -using agent::makeAgent; -using agent::makeToolRegistry; -using agent::ReActAgent; -using agent::StepCallback; -using agent::ToolApprovalCallback; -using agent::ToolEntry; -using agent::ToolExecution; -using agent::ToolFunction; -using agent::ToolRegistry; -using agent::ToolRegistryPtr; -using agent::toServerToolInfo; // Convert ToolSpec -> ServerToolInfo -using agent::toToolSpec; // Convert ServerToolInfo -> ToolSpec -namespace AgentError = agent::AgentError; // Namespace alias for error codes - -// Re-export Tool Definition and Config types -using agent::AuthPreset; -using agent::ConfigLoader; -using agent::makeRESTToolAdapter; -using agent::MCPServerDefinition; -using agent::RegistryConfig; -using agent::RESTToolAdapter; -using agent::RESTToolAdapterPtr; -using agent::ToolDefinition; - -// FFI C++ utilities (conditional) -// The C API (gopher_orch_*) is always available in the global namespace -#ifdef GOPHER_ORCH_WITH_FFI -namespace ffi_utils = ffi; // Alias for FFI RAII utilities -#endif - -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/resilience/circuit_breaker.h b/third_party/gopher-orch/include/gopher/orch/resilience/circuit_breaker.h deleted file mode 100644 index 9b9784b5..00000000 --- a/third_party/gopher-orch/include/gopher/orch/resilience/circuit_breaker.h +++ /dev/null @@ -1,250 +0,0 @@ -#pragma once - -// CircuitBreaker - Prevent cascade failures -// Implements the circuit breaker pattern to stop calling failing services -// -// States: -// - CLOSED: Normal operation, requests pass through -// - OPEN: Failures exceeded threshold, requests immediately rejected -// - HALF_OPEN: Testing if service recovered, limited requests allowed -// -// Behavior: -// - Tracks failures and opens circuit when threshold reached -// - Rejects requests immediately when open (fail-fast) -// - Tries limited requests after recovery timeout (half-open) -// - Closes circuit when half-open requests succeed - -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace resilience { - -using namespace gopher::orch::core; - -// CircuitBreaker states -enum class CircuitState { CLOSED, OPEN, HALF_OPEN }; - -// CircuitBreakerPolicy - Configuration for circuit breaker behavior -struct CircuitBreakerPolicy { - uint32_t failure_threshold; // Number of failures before opening - uint64_t recovery_timeout_ms; // Time to wait before trying half-open - uint32_t half_open_max_calls; // Number of successful calls to close - - // Optional: callback for state changes (for logging/observability) - std::function on_state_change; - - CircuitBreakerPolicy() - : failure_threshold(5), - recovery_timeout_ms(30000), - half_open_max_calls(3), - on_state_change(nullptr) {} - - // Factory for common configurations - static CircuitBreakerPolicy standard() { return CircuitBreakerPolicy(); } - - static CircuitBreakerPolicy aggressive(uint32_t failure_threshold = 3, - uint64_t recovery_timeout_ms = 10000) { - CircuitBreakerPolicy policy; - policy.failure_threshold = failure_threshold; - policy.recovery_timeout_ms = recovery_timeout_ms; - return policy; - } - - static CircuitBreakerPolicy lenient(uint32_t failure_threshold = 10, - uint64_t recovery_timeout_ms = 60000) { - CircuitBreakerPolicy policy; - policy.failure_threshold = failure_threshold; - policy.recovery_timeout_ms = recovery_timeout_ms; - return policy; - } -}; - -// CircuitBreaker - Prevent cascade failures -template -class CircuitBreaker : public Runnable { - public: - using RunnablePtr = std::shared_ptr>; - using Callback = typename Runnable::Callback; - - CircuitBreaker(RunnablePtr inner, CircuitBreakerPolicy policy) - : inner_(std::move(inner)), - policy_(std::move(policy)), - state_(CircuitState::CLOSED), - failure_count_(0), - half_open_successes_(0), - last_failure_time_(0) {} - - std::string name() const override { - return "CircuitBreaker(" + inner_->name() + ")"; - } - - // Get current circuit state - CircuitState state() const { return state_.load(); } - - // Get failure count - uint32_t failureCount() const { return failure_count_.load(); } - - void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Check and potentially transition state - CircuitState current_state = checkAndTransitionState(); - - if (current_state == CircuitState::OPEN) { - // Circuit is open - fail fast - dispatcher.post([callback = std::move(callback)]() { - callback( - makeOrchError(OrchError::CIRCUIT_OPEN, "Circuit is open")); - }); - return; - } - - // Circuit is closed or half-open - try the operation - auto self = std::static_pointer_cast>( - this->shared_from_this()); - - inner_->invoke( - input, config.child(), dispatcher, - [self, callback = std::move(callback)](Result result) mutable { - if (mcp::holds_alternative(result)) { - self->onSuccess(); - } else { - self->onFailure(); - } - callback(std::move(result)); - }); - } - - // Manual reset of circuit breaker (for testing/admin purposes) - void reset() { - std::lock_guard lock(mutex_); - transitionTo(CircuitState::CLOSED); - failure_count_.store(0); - half_open_successes_.store(0); - } - - // Factory method - static std::shared_ptr> create( - RunnablePtr inner, CircuitBreakerPolicy policy = CircuitBreakerPolicy()) { - return std::make_shared>(std::move(inner), - std::move(policy)); - } - - private: - // Check current state and transition if needed (e.g., OPEN -> HALF_OPEN) - CircuitState checkAndTransitionState() { - CircuitState current = state_.load(); - - if (current == CircuitState::OPEN) { - // Check if recovery timeout has elapsed - uint64_t now = currentTimeMs(); - uint64_t last_failure = last_failure_time_.load(); - uint64_t elapsed = now - last_failure; - - if (elapsed >= policy_.recovery_timeout_ms) { - // Try to transition to HALF_OPEN - std::lock_guard lock(mutex_); - if (state_.load() == CircuitState::OPEN) { - transitionTo(CircuitState::HALF_OPEN); - half_open_successes_.store(0); - return CircuitState::HALF_OPEN; - } - } - } - - return state_.load(); - } - - // Called when operation succeeds - void onSuccess() { - std::lock_guard lock(mutex_); - - CircuitState current = state_.load(); - if (current == CircuitState::HALF_OPEN) { - // Count successful calls in half-open state - uint32_t successes = ++half_open_successes_; - if (successes >= policy_.half_open_max_calls) { - // Enough successes - close the circuit - transitionTo(CircuitState::CLOSED); - failure_count_.store(0); - } - } else { - // Reset failure count on success - failure_count_.store(0); - } - } - - // Called when operation fails - void onFailure() { - std::lock_guard lock(mutex_); - - CircuitState current = state_.load(); - if (current == CircuitState::HALF_OPEN) { - // Failure in half-open - immediately reopen - transitionTo(CircuitState::OPEN); - last_failure_time_.store(currentTimeMs()); - } else { - // Count failure and potentially open circuit - uint32_t failures = ++failure_count_; - if (failures >= policy_.failure_threshold) { - transitionTo(CircuitState::OPEN); - last_failure_time_.store(currentTimeMs()); - } - } - } - - // Transition to new state with optional callback - void transitionTo(CircuitState new_state) { - CircuitState old_state = state_.exchange(new_state); - if (old_state != new_state && policy_.on_state_change) { - policy_.on_state_change(old_state, new_state); - } - } - - // Get current time in milliseconds - static uint64_t currentTimeMs() { - auto now = std::chrono::steady_clock::now(); - auto ms = std::chrono::duration_cast( - now.time_since_epoch()); - return static_cast(ms.count()); - } - - RunnablePtr inner_; - CircuitBreakerPolicy policy_; - std::atomic state_; - std::atomic failure_count_; - std::atomic half_open_successes_; - std::atomic last_failure_time_; - std::mutex mutex_; -}; - -// Convenience alias for JSON circuit breaker -using JsonCircuitBreaker = CircuitBreaker; - -// Factory function for creating circuit breaker wrapper -template -std::shared_ptr> withCircuitBreaker( - std::shared_ptr> inner, - CircuitBreakerPolicy policy = CircuitBreakerPolicy()) { - return CircuitBreaker::create(std::move(inner), std::move(policy)); -} - -// Factory for JSON circuit breaker -inline std::shared_ptr withCircuitBreaker( - JsonRunnablePtr inner, - CircuitBreakerPolicy policy = CircuitBreakerPolicy()) { - return JsonCircuitBreaker::create(std::move(inner), std::move(policy)); -} - -} // namespace resilience -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/resilience/fallback.h b/third_party/gopher-orch/include/gopher/orch/resilience/fallback.h deleted file mode 100644 index 301587f9..00000000 --- a/third_party/gopher-orch/include/gopher/orch/resilience/fallback.h +++ /dev/null @@ -1,155 +0,0 @@ -#pragma once - -// Fallback - Try alternatives on failure -// Chains multiple runnables and tries each in order until one succeeds -// -// Behavior: -// - Tries primary runnable first -// - On failure, tries each fallback in order -// - Returns first successful result -// - Returns FALLBACK_EXHAUSTED error if all fail - -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace resilience { - -using namespace gopher::orch::core; - -// Fallback - Try alternatives on failure -template -class Fallback : public Runnable { - public: - using RunnablePtr = std::shared_ptr>; - using Callback = typename Runnable::Callback; - - Fallback(RunnablePtr primary, std::vector fallbacks) - : primary_(std::move(primary)), fallbacks_(std::move(fallbacks)) {} - - std::string name() const override { - std::string result = "Fallback(" + primary_->name(); - for (const auto& fb : fallbacks_) { - result += " -> " + fb->name(); - } - result += ")"; - return result; - } - - void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Start with primary (index 0) - attemptInvoke(input, config, dispatcher, std::move(callback), 0); - } - - // Get the primary runnable - RunnablePtr primary() const { return primary_; } - - // Get fallback runnables - const std::vector& fallbacks() const { return fallbacks_; } - - // Factory method - static std::shared_ptr> create( - RunnablePtr primary, std::vector fallbacks) { - return std::make_shared>(std::move(primary), - std::move(fallbacks)); - } - - private: - void attemptInvoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback, - size_t index) { - // Get current runnable to try - RunnablePtr current; - if (index == 0) { - current = primary_; - } else if (index <= fallbacks_.size()) { - current = fallbacks_[index - 1]; - } else { - // All fallbacks exhausted - dispatcher.post([callback = std::move(callback)]() { - callback(makeOrchError(OrchError::FALLBACK_EXHAUSTED, - "All fallback options failed")); - }); - return; - } - - auto self = std::static_pointer_cast>( - this->shared_from_this()); - auto input_copy = input; // Copy for potential fallback - - current->invoke( - input, config.child(), dispatcher, - [self, input_copy, config, &dispatcher, callback = std::move(callback), - index](Result result) mutable { - if (mcp::holds_alternative(result)) { - // Success - return result - callback(std::move(result)); - return; - } - - // Failure - try next fallback - self->attemptInvoke(input_copy, config, dispatcher, - std::move(callback), index + 1); - }); - } - - RunnablePtr primary_; - std::vector fallbacks_; -}; - -// Convenience alias for JSON fallback -using JsonFallback = Fallback; - -// Builder for creating Fallback with fluent API -template -class FallbackBuilder { - public: - using RunnablePtr = std::shared_ptr>; - - explicit FallbackBuilder(RunnablePtr primary) - : primary_(std::move(primary)) {} - - // Add a fallback option - FallbackBuilder& orElse(RunnablePtr fallback) { - fallbacks_.push_back(std::move(fallback)); - return *this; - } - - std::shared_ptr> build() { - return Fallback::create(std::move(primary_), - std::move(fallbacks_)); - } - - // Implicit conversion to shared_ptr - operator std::shared_ptr>() { return build(); } - - private: - RunnablePtr primary_; - std::vector fallbacks_; -}; - -// Factory function for creating fallback builder -template -FallbackBuilder withFallback(std::shared_ptr> primary) { - return FallbackBuilder(std::move(primary)); -} - -// Factory for JSON fallback builder -inline FallbackBuilder withFallback( - JsonRunnablePtr primary) { - return FallbackBuilder(std::move(primary)); -} - -} // namespace resilience -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/resilience/retry.h b/third_party/gopher-orch/include/gopher/orch/resilience/retry.h deleted file mode 100644 index 919333d6..00000000 --- a/third_party/gopher-orch/include/gopher/orch/resilience/retry.h +++ /dev/null @@ -1,208 +0,0 @@ -#pragma once - -// Retry - Wrap a runnable with retry logic -// Implements exponential backoff with optional jitter -// -// Behavior: -// - Retries failed operations up to max_attempts times -// - Delays between retries using exponential backoff -// - Optional jitter to prevent thundering herd -// - Optional retry condition to filter retryable errors - -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace resilience { - -using namespace gopher::orch::core; - -// RetryPolicy - Configuration for retry behavior -struct RetryPolicy { - uint32_t max_attempts; // Maximum number of attempts (including first) - uint64_t initial_delay_ms; // Initial delay before first retry - double backoff_multiplier; // Multiplier for each subsequent retry - uint64_t max_delay_ms; // Maximum delay between retries - bool jitter; // Add random jitter to delays - - // Optional: condition to check if error is retryable - std::function retry_on; - - // Optional: callback on retry (for logging/observability) - std::function on_retry; - - RetryPolicy() - : max_attempts(3), - initial_delay_ms(500), - backoff_multiplier(2.0), - max_delay_ms(30000), - jitter(true), - retry_on(nullptr), - on_retry(nullptr) {} - - // Factory for exponential backoff policy - static RetryPolicy exponential(uint32_t attempts = 3, - uint64_t initial_delay_ms = 500) { - RetryPolicy policy; - policy.max_attempts = attempts; - policy.initial_delay_ms = initial_delay_ms; - return policy; - } - - // Factory for fixed delay policy (no backoff) - static RetryPolicy fixed(uint32_t attempts, uint64_t delay_ms) { - RetryPolicy policy; - policy.max_attempts = attempts; - policy.initial_delay_ms = delay_ms; - policy.backoff_multiplier = 1.0; - policy.jitter = false; - return policy; - } -}; - -// Retry - Wrap a runnable with retry logic -template -class Retry : public Runnable { - public: - using RunnablePtr = std::shared_ptr>; - using Callback = typename Runnable::Callback; - - Retry(RunnablePtr inner, RetryPolicy policy) - : inner_(std::move(inner)), policy_(std::move(policy)) {} - - std::string name() const override { - return "Retry(" + inner_->name() + ", " + - std::to_string(policy_.max_attempts) + ")"; - } - - void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Start first attempt - attemptInvoke(input, config, dispatcher, std::move(callback), 1); - } - - // Factory method - static std::shared_ptr> create(RunnablePtr inner, - RetryPolicy policy) { - return std::make_shared>(std::move(inner), - std::move(policy)); - } - - private: - // State to hold timer during retry delay - // This ensures timer is kept alive until it fires - struct RetryState { - mcp::event::TimerPtr timer; - }; - - void attemptInvoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback, - uint32_t attempt) { - auto self = std::static_pointer_cast>( - this->shared_from_this()); - auto input_copy = input; // Copy for potential retry - - inner_->invoke( - input, config.child(), dispatcher, - [self, input_copy, config, &dispatcher, callback = std::move(callback), - attempt](Result result) mutable { - if (mcp::holds_alternative(result)) { - // Success - return result - callback(std::move(result)); - return; - } - - // Get error for retry decision - const auto& error = mcp::get(result); - - // Check if we should retry - bool should_retry = attempt < self->policy_.max_attempts; - - // Check optional retry condition - if (should_retry && self->policy_.retry_on) { - should_retry = self->policy_.retry_on(error); - } - - if (!should_retry) { - // No more retries - return error - callback(std::move(result)); - return; - } - - // Invoke optional retry callback - if (self->policy_.on_retry) { - self->policy_.on_retry(error, attempt); - } - - // Calculate delay with exponential backoff - uint64_t delay_ms = self->calculateDelay(attempt); - - // Create state to hold timer (keeps timer alive until callback fires) - auto state = std::make_shared(); - - // Schedule retry after delay using timer - state->timer = dispatcher.createTimer( - [self, input_copy, config, &dispatcher, - callback = std::move(callback), attempt, state]() mutable { - // State is captured to keep timer alive until this point - self->attemptInvoke(input_copy, config, dispatcher, - std::move(callback), attempt + 1); - }); - state->timer->enableTimer(std::chrono::milliseconds(delay_ms)); - }); - } - - uint64_t calculateDelay(uint32_t attempt) const { - // Calculate base delay with exponential backoff - double delay = policy_.initial_delay_ms * - std::pow(policy_.backoff_multiplier, attempt - 1); - - // Cap at max delay - if (delay > static_cast(policy_.max_delay_ms)) { - delay = static_cast(policy_.max_delay_ms); - } - - // Add jitter if enabled (±50%) - if (policy_.jitter) { - static thread_local std::mt19937 gen(std::random_device{}()); - std::uniform_real_distribution<> dis(0.5, 1.5); - delay *= dis(gen); - } - - return static_cast(delay); - } - - RunnablePtr inner_; - RetryPolicy policy_; -}; - -// Convenience alias for JSON retry -using JsonRetry = Retry; - -// Factory function for creating retry wrapper -template -std::shared_ptr> withRetry(std::shared_ptr> inner, - RetryPolicy policy = RetryPolicy()) { - return Retry::create(std::move(inner), std::move(policy)); -} - -// Factory for JSON retry -inline std::shared_ptr withRetry( - JsonRunnablePtr inner, RetryPolicy policy = RetryPolicy()) { - return JsonRetry::create(std::move(inner), std::move(policy)); -} - -} // namespace resilience -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/resilience/timeout.h b/third_party/gopher-orch/include/gopher/orch/resilience/timeout.h deleted file mode 100644 index bbe7e8cf..00000000 --- a/third_party/gopher-orch/include/gopher/orch/resilience/timeout.h +++ /dev/null @@ -1,130 +0,0 @@ -#pragma once - -// Timeout - Limit execution time for a runnable -// Wraps a runnable and returns error if it doesn't complete within timeout -// -// Behavior: -// - Starts timer when invoke is called -// - Returns TIMEOUT error if timer fires before operation completes -// - Disables timer and returns result if operation completes first -// - Thread-safe handling of race between timer and completion - -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace resilience { - -using namespace gopher::orch::core; - -// Timeout - Wrap a runnable with timeout limit -template -class Timeout : public Runnable { - public: - using RunnablePtr = std::shared_ptr>; - using Callback = typename Runnable::Callback; - - Timeout(RunnablePtr inner, uint64_t timeout_ms) - : inner_(std::move(inner)), timeout_ms_(timeout_ms) {} - - std::string name() const override { - return "Timeout(" + inner_->name() + ", " + std::to_string(timeout_ms_) + - "ms)"; - } - - void invoke(const Input& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - // Create shared state to coordinate between timer and operation - auto state = std::make_shared(std::move(callback)); - - // Start timeout timer - // We need to keep the timer alive, so store it in the state - state->timer = dispatcher.createTimer( - [state, &dispatcher]() { state->onTimeout(dispatcher); }); - state->timer->enableTimer(std::chrono::milliseconds(timeout_ms_)); - - // Invoke inner runnable - inner_->invoke(input, config.child(), dispatcher, - [state, &dispatcher](Result result) { - state->onResult(std::move(result), dispatcher); - }); - } - - // Factory method - static std::shared_ptr> create(RunnablePtr inner, - uint64_t timeout_ms) { - return std::make_shared>(std::move(inner), - timeout_ms); - } - - private: - // Shared state for coordinating between timeout and completion - struct TimeoutState { - Callback callback; - mcp::event::TimerPtr timer; - std::atomic completed{false}; - - explicit TimeoutState(Callback cb) : callback(std::move(cb)) {} - - // Called when the operation completes (success or failure) - void onResult(Result result, Dispatcher& dispatcher) { - bool expected = false; - if (completed.compare_exchange_strong(expected, true)) { - // We won the race - disable timer and deliver result - if (timer) { - timer->disableTimer(); - } - // Post to dispatcher to ensure callback runs in dispatcher context - auto cb = std::move(callback); - dispatcher.post( - [cb = std::move(cb), result = std::move(result)]() mutable { - cb(std::move(result)); - }); - } - // else: timeout already fired, discard result - } - - // Called when the timeout fires - void onTimeout(Dispatcher& dispatcher) { - bool expected = false; - if (completed.compare_exchange_strong(expected, true)) { - // We won the race - deliver timeout error - auto cb = std::move(callback); - dispatcher.post([cb = std::move(cb)]() { - cb(makeOrchError(OrchError::TIMEOUT, "Operation timed out")); - }); - } - // else: operation already completed, ignore timeout - } - }; - - RunnablePtr inner_; - uint64_t timeout_ms_; -}; - -// Convenience alias for JSON timeout -using JsonTimeout = Timeout; - -// Factory function for creating timeout wrapper -template -std::shared_ptr> withTimeout( - std::shared_ptr> inner, uint64_t timeout_ms) { - return Timeout::create(std::move(inner), timeout_ms); -} - -// Factory for JSON timeout -inline std::shared_ptr withTimeout(JsonRunnablePtr inner, - uint64_t timeout_ms) { - return JsonTimeout::create(std::move(inner), timeout_ms); -} - -} // namespace resilience -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/server/gateway_server.h b/third_party/gopher-orch/include/gopher/orch/server/gateway_server.h deleted file mode 100644 index 2a86c2c3..00000000 --- a/third_party/gopher-orch/include/gopher/orch/server/gateway_server.h +++ /dev/null @@ -1,207 +0,0 @@ -#pragma once - -// GatewayServer - MCP server that exposes tools from multiple backend servers -// -// Creates an MCP server that acts as a gateway/proxy, exposing tools from -// multiple backend MCP servers to external clients through a single endpoint. -// -// Architecture: -// External MCP Clients -// | -// v -// +-------------------+ -// | GatewayServer | (MCP Server on configurable port) -// | | -// | ServerComposite | --> Backend Server 1 (e.g., localhost:3001) -// | | --> Backend Server 2 (e.g., localhost:3002) -// +-------------------+ -// -// Simple Usage: -// std::string serverJson = R"({ -// "succeeded": true, -// "data": { -// "servers": [ -// {"name": "server1", "transport": "http_sse", -// "config": {"url": "http://127.0.0.1:3001/rpc"}} -// ] -// } -// })"; -// -// auto gateway = GatewayServer::create(serverJson); -// gateway->listen(3003); // Blocks until Ctrl+C -// -// Advanced Usage: -// auto composite = ServerComposite::create("backends"); -// composite->addServer(server1, tool_names1, false); -// -// GatewayServerConfig config; -// config.port = 3003; -// -// auto gateway = GatewayServer::create(composite, config); -// gateway->start(dispatcher, callback); - -#include -#include -#include -#include -#include - -#include "gopher/orch/core/types.h" -#include "gopher/orch/server/server_composite.h" -#include "mcp/server/mcp_server.h" - -namespace gopher { -namespace orch { -namespace server { - -using namespace gopher::orch::core; - -// Forward declaration -class GatewayServer; -using GatewayServerPtr = std::shared_ptr; - -// Configuration for GatewayServer -struct GatewayServerConfig { - std::string name = "gateway-server"; - std::string host = "0.0.0.0"; - int port = 3003; - int workers = 4; - int max_sessions = 100; - std::chrono::milliseconds session_timeout{300000}; - std::chrono::milliseconds request_timeout{30000}; - - // HTTP/SSE paths - std::string http_rpc_path = "/mcp"; - std::string http_sse_path = "/events"; - std::string http_health_path = "/health"; -}; - -// GatewayServer - MCP server exposing tools from multiple backend servers -// -// Thread Safety: -// - listen() blocks the calling thread -// - start() should be called once -// - stop() can be called from any thread -// - isRunning() is thread-safe -class GatewayServer : public std::enable_shared_from_this { - public: - using Ptr = std::shared_ptr; - - // ═══════════════════════════════════════════════════════════════════════════ - // SIMPLE API - Create from JSON and listen - // ═══════════════════════════════════════════════════════════════════════════ - - // Create a GatewayServer from JSON server configuration - // The JSON format matches the API response format with "data.servers" array - static Ptr create(const std::string& serverJson, - const GatewayServerConfig& config = {}); - - // Start listening and block until shutdown (Ctrl+C or stop() called) - // This is the simplest way to run the gateway server - // Returns 0 on success, non-zero on error - int listen(int port); - - // ═══════════════════════════════════════════════════════════════════════════ - // ADVANCED API - Create from ServerComposite with async control - // ═══════════════════════════════════════════════════════════════════════════ - - // Create a GatewayServer with an existing ServerComposite - static Ptr create(ServerCompositePtr composite, - const GatewayServerConfig& config = {}) { - return std::shared_ptr( - new GatewayServer(std::move(composite), config)); - } - - // Start the gateway server asynchronously - void start(Dispatcher& dispatcher, std::function callback); - - // Stop the gateway server - void stop(Dispatcher& dispatcher, std::function callback); - - // Stop the gateway server (blocking) - void stop(); - - // ═══════════════════════════════════════════════════════════════════════════ - // ACCESSORS - // ═══════════════════════════════════════════════════════════════════════════ - - ~GatewayServer(); - - // Get the server name - const std::string& name() const { return config_.name; } - - // Get the underlying ServerComposite - ServerCompositePtr getComposite() const { return composite_; } - - // Check if server is running - bool isRunning() const { return running_.load(); } - - // Get the listen address for display (e.g., "0.0.0.0:3003") - std::string getListenAddress() const { - return config_.host + ":" + std::to_string(config_.port); - } - - // Get the listen URL for MCP server (e.g., "http://0.0.0.0:3003") - std::string getListenUrl() const { - return "http://" + config_.host + ":" + std::to_string(config_.port); - } - - // Get the number of registered tools - size_t toolCount() const { return tool_count_.load(); } - - // Get the number of connected backend servers - size_t serverCount() const; - - // Get error message if creation failed - const std::string& getError() const { return error_message_; } - - private: - explicit GatewayServer(ServerCompositePtr composite, - const GatewayServerConfig& config); - - // Private constructor for JSON-based creation (sets error_message_ on failure) - GatewayServer(const GatewayServerConfig& config); - - // Initialize from JSON (called by create()) - bool initFromJson(const std::string& serverJson); - - // Register tools from composite onto the MCP server - void registerToolsFromComposite(); - - // Create a tool handler that routes through the composite - mcp::CallToolResult handleToolCall(const std::string& tool_name, - const mcp::optional& arguments, - mcp::server::SessionContext& session); - - ServerCompositePtr composite_; - GatewayServerConfig config_; - std::unique_ptr mcp_server_; - std::unique_ptr owned_dispatcher_; // For simple API - std::atomic running_{false}; - std::atomic shutdown_requested_{false}; - std::atomic tool_count_{0}; - std::string error_message_; - - // Cached tool info from all servers (includes descriptions and schemas) - std::vector cached_tool_infos_; - - // Background thread for running the backend dispatcher - std::unique_ptr dispatcher_thread_; - std::atomic dispatcher_running_{false}; -}; - -// Convenience function to create a gateway server from JSON -inline GatewayServerPtr makeGatewayServer(const std::string& serverJson, - const GatewayServerConfig& config = {}) { - return GatewayServer::create(serverJson, config); -} - -// Convenience function to create a gateway server from composite -inline GatewayServerPtr makeGatewayServer(ServerCompositePtr composite, - const GatewayServerConfig& config = {}) { - return GatewayServer::create(std::move(composite), config); -} - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/server/mcp_server.h b/third_party/gopher-orch/include/gopher/orch/server/mcp_server.h deleted file mode 100644 index ff8365f8..00000000 --- a/third_party/gopher-orch/include/gopher/orch/server/mcp_server.h +++ /dev/null @@ -1,202 +0,0 @@ -#pragma once - -// MCPServer - MCP protocol implementation of Server interface -// -// Wraps the gopher-mcp client to provide a protocol-agnostic Server interface. -// Supports stdio, HTTP+SSE, and WebSocket transports. -// -// Usage: -// MCPServerConfig config; -// config.name = "my-mcp-server"; -// config.transport = MCPServerConfig::StdioTransport{"npx", {"-y", -// "server"}}; -// -// MCPServer::create(config, dispatcher, [](Result result) { -// if (result.isOk()) { -// auto server = result.value(); -// // Use server->tool("tool_name") to get a Runnable -// } -// }); - -#include -#include -#include -#include -#include -#include - -#include "mcp/client/mcp_client.h" -#include "mcp/event/event_loop.h" -#include "mcp/types.h" - -#include "gopher/orch/server/server.h" - -namespace gopher { -namespace orch { -namespace server { - -// Forward declaration -class MCPServer; -using MCPServerPtr = std::shared_ptr; - -// Configuration for MCP server connection -struct MCPServerConfig { - std::string name; // Human-readable name for this server - - // Stdio transport configuration - // Used for subprocess-based MCP servers (most common) - struct StdioTransport { - std::string command; // Command to run - std::vector args; // Command arguments - std::map env; // Environment variables - std::string working_directory; // Working directory (optional) - }; - - // HTTP+SSE transport configuration - // Used for network-based MCP servers - struct HttpSseTransport { - std::string url; // Server URL (e.g., "http://localhost:8080") - std::map headers; // HTTP headers - bool verify_ssl = true; // Verify SSL certificates - }; - - // WebSocket transport configuration (future) - struct WebSocketTransport { - std::string url; // WebSocket URL - std::map headers; // HTTP headers for upgrade - bool verify_ssl = true; // Verify SSL certificates - }; - - // Transport configuration - one of the above - // Use std::variant when C++17 is available, otherwise use tagged union - // pattern - enum class TransportType { STDIO, HTTP_SSE, WEBSOCKET }; - TransportType transport_type = TransportType::STDIO; - StdioTransport stdio_transport; - HttpSseTransport http_sse_transport; - WebSocketTransport websocket_transport; - - // Connection timeouts - std::chrono::milliseconds connect_timeout{30000}; - std::chrono::milliseconds request_timeout{60000}; - - // Retry configuration for initial connection - uint32_t max_connect_retries = 3; - std::chrono::milliseconds retry_delay{1000}; - - // Client info for MCP initialization - std::string client_name = "gopher-orch"; - std::string client_version = "1.0.0"; -}; - -// MCPServer - MCP protocol implementation of Server interface -// -// Thread Safety: -// - All public methods must be called from dispatcher thread -// - Callbacks are invoked in dispatcher thread context -// - connect() initiates async connection, callback when complete -// -// Lifecycle: -// - Create with MCPServer::create() factory method -// - connect() starts connection and protocol initialization -// - Once connected, use tool() to get Runnables for tools -// - disconnect() gracefully shuts down -class MCPServer : public Server { - public: - // Factory method - creates and optionally auto-connects - // - // If auto_connect is true (default), the server will start connecting - // immediately and the callback is invoked when ready or on error. - // - // If auto_connect is false, the callback is invoked immediately with - // the created server, and you must call connect() explicitly. - static void create(const MCPServerConfig& config, - Dispatcher& dispatcher, - std::function)> callback, - bool auto_connect = true); - - ~MCPServer() override; - - // Server interface implementation - std::string id() const override { return id_; } - std::string name() const override { return config_.name; } - ConnectionState connectionState() const override { return state_; } - - void connect(Dispatcher& dispatcher, ConnectionCallback callback) override; - void disconnect(Dispatcher& dispatcher, - std::function callback) override; - - void listTools(Dispatcher& dispatcher, - ServerToolListCallback callback) override; - - JsonRunnablePtr tool(const std::string& name) override; - - void callTool(const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) override; - - // MCP-specific accessors - - // Server information from initialization response - const mcp::Implementation& serverInfo() const { return server_info_; } - - // Server capabilities from initialization response - const mcp::ServerCapabilities& capabilities() const { return capabilities_; } - - // Get the underlying MCP client (for advanced usage) - mcp::client::McpClient* client() const { return client_.get(); } - - private: - // Private constructor - use create() factory - explicit MCPServer(const MCPServerConfig& config); - - // Initialize the MCP connection - // Called after create() if auto_connect is true - void initialize(Dispatcher& dispatcher, - std::function)> callback); - - // Handle connection established - void onConnected(Dispatcher& dispatcher, - std::function)> callback); - - // Handle protocol initialization complete - void onInitialized(Dispatcher& dispatcher, - const mcp::InitializeResult& init_result, - std::function)> callback, - std::shared_ptr start_time, - std::chrono::milliseconds timeout); - - // Handle tools listed - void onToolsListed(const mcp::ListToolsResult& tools_result); - - // Convert MCP Tool to ServerToolInfo - static ServerToolInfo toServerToolInfo(const mcp::Tool& tool); - - // Convert MCP content to JsonValue - static JsonValue contentToJson( - const std::vector& content); - - // Generate unique ID - static std::string generateId(); - - std::string id_; - MCPServerConfig config_; - ConnectionState state_ = ConnectionState::DISCONNECTED; - - std::unique_ptr client_; - mcp::Implementation server_info_; - mcp::ServerCapabilities capabilities_; - - // Cached tool information - std::vector tools_; - std::map tool_cache_; - - // Pending callbacks during connection - std::vector> pending_on_connect_; -}; - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/server/mock_server.h b/third_party/gopher-orch/include/gopher/orch/server/mock_server.h deleted file mode 100644 index abbb2c5c..00000000 --- a/third_party/gopher-orch/include/gopher/orch/server/mock_server.h +++ /dev/null @@ -1,274 +0,0 @@ -#pragma once - -// MockServer - In-memory server implementation for testing -// -// Provides a server that operates entirely in memory with no network I/O. -// Useful for: -// - Unit testing workflows without network dependencies -// - Mocking specific tool behaviors -// - Recording tool calls for verification -// - Simulating errors and edge cases - -#include -#include -#include -#include -#include - -#include "gopher/orch/server/server.h" - -namespace gopher { -namespace orch { -namespace server { - -// Mock tool response configuration -struct MockToolConfig { - // Response to return on success - optional response; - - // Error to return (if set, overrides response) - optional error; - - // Delay before responding (in milliseconds) - std::chrono::milliseconds delay{0}; - - // Number of calls received - size_t call_count = 0; - - // Last arguments received - optional last_arguments; - - // Custom handler (overrides response/error if set) - std::function(const JsonValue&)> handler; -}; - -// MockServer - In-memory server for testing -class MockServer : public Server { - public: - explicit MockServer(const std::string& name, const std::string& id = "") - : name_(name), - id_(id.empty() ? "mock-" + name : id), - state_(ConnectionState::DISCONNECTED) {} - - // Server interface implementation - std::string id() const override { return id_; } - std::string name() const override { return name_; } - ConnectionState connectionState() const override { return state_; } - - void connect(Dispatcher& dispatcher, ConnectionCallback callback) override { - state_ = ConnectionState::CONNECTED; - dispatcher.post([callback]() { callback(makeSuccess(nullptr)); }); - } - - void disconnect(Dispatcher& dispatcher, - std::function callback) override { - state_ = ConnectionState::DISCONNECTED; - if (callback) { - dispatcher.post(std::move(callback)); - } - } - - void listTools(Dispatcher& dispatcher, - ServerToolListCallback callback) override { - std::vector tools; - { - std::lock_guard lock(mutex_); - for (const auto& kv : tools_) { - tools.push_back(kv.second); - } - } - dispatcher.post([tools = std::move(tools), callback]() { - callback(makeSuccess(std::move(tools))); - }); - } - - JsonRunnablePtr tool(const std::string& name) override { - std::lock_guard lock(mutex_); - auto it = tools_.find(name); - if (it == tools_.end()) { - return nullptr; - } - return std::make_shared(shared(), it->second); - } - - void callTool(const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) override { - MockToolConfig* tool_config = nullptr; - { - std::lock_guard lock(mutex_); - auto it = configs_.find(name); - if (it == configs_.end()) { - auto tool_it = tools_.find(name); - if (tool_it == tools_.end()) { - dispatcher.post([name, callback]() { - callback(Result( - Error(OrchError::TOOL_NOT_FOUND, "Tool not found: " + name))); - }); - return; - } - // Create default config for tool - configs_[name] = MockToolConfig(); - configs_[name].response = JsonValue::object(); - it = configs_.find(name); - } - tool_config = &it->second; - tool_config->call_count++; - tool_config->last_arguments = arguments; - } - - // Capture result before posting - Result result = Result(JsonValue::object()); - - if (tool_config->handler) { - result = tool_config->handler(arguments); - } else if (tool_config->error.has_value()) { - result = Result(tool_config->error.value()); - } else if (tool_config->response.has_value()) { - // Copy the response value to avoid reference issues - JsonValue response_copy = tool_config->response.value(); - result = Result(std::move(response_copy)); - } - - auto delay = tool_config->delay; - - if (delay.count() > 0) { - // Create timer for delayed response - auto timer = - dispatcher.createTimer([result = std::move(result), - callback = std::move(callback)]() mutable { - callback(std::move(result)); - }); - timer->enableTimer(delay); - } else { - dispatcher.post([result = std::move(result), - callback = std::move(callback)]() mutable { - callback(std::move(result)); - }); - } - } - - // ========================================================================= - // MockServer-specific API for test configuration - // ========================================================================= - - // Add a tool to the mock server - MockServer& addTool(const std::string& name, - const std::string& description = "") { - std::lock_guard lock(mutex_); - tools_[name] = ServerToolInfo(name, description); - return *this; - } - - // Add a tool with schema - MockServer& addTool(const ServerToolInfo& info) { - std::lock_guard lock(mutex_); - tools_[info.name] = info; - return *this; - } - - // Set the response for a tool - MockServer& setResponse(const std::string& toolName, - const JsonValue& response) { - std::lock_guard lock(mutex_); - configs_[toolName].response = response; - configs_[toolName].error = nullopt; - return *this; - } - - // Set an error response for a tool - MockServer& setError(const std::string& toolName, const Error& error) { - std::lock_guard lock(mutex_); - configs_[toolName].error = error; - return *this; - } - - MockServer& setError(const std::string& toolName, - int code, - const std::string& message) { - return setError(toolName, Error(code, message)); - } - - // Set a delay before responding - MockServer& setDelay(const std::string& toolName, - std::chrono::milliseconds delay) { - std::lock_guard lock(mutex_); - configs_[toolName].delay = delay; - return *this; - } - - // Set a custom handler for a tool - MockServer& setHandler( - const std::string& toolName, - std::function(const JsonValue&)> handler) { - std::lock_guard lock(mutex_); - configs_[toolName].handler = std::move(handler); - return *this; - } - - // Get call count for a tool - size_t callCount(const std::string& toolName) const { - std::lock_guard lock(mutex_); - auto it = configs_.find(toolName); - if (it == configs_.end()) { - return 0; - } - return it->second.call_count; - } - - // Get total call count for all tools - size_t totalCallCount() const { - std::lock_guard lock(mutex_); - size_t total = 0; - for (const auto& kv : configs_) { - total += kv.second.call_count; - } - return total; - } - - // Get last arguments for a tool - optional lastArguments(const std::string& toolName) const { - std::lock_guard lock(mutex_); - auto it = configs_.find(toolName); - if (it == configs_.end()) { - return nullopt; - } - return it->second.last_arguments; - } - - // Reset all call counts - void resetCallCounts() { - std::lock_guard lock(mutex_); - for (auto& kv : configs_) { - kv.second.call_count = 0; - kv.second.last_arguments = nullopt; - } - } - - // Clear all tools and configs - void clear() { - std::lock_guard lock(mutex_); - tools_.clear(); - configs_.clear(); - } - - private: - mutable std::mutex mutex_; - std::string name_; - std::string id_; - ConnectionState state_; - std::map tools_; - std::map configs_; -}; - -// Factory function -inline std::shared_ptr makeMockServer(const std::string& name, - const std::string& id = "") { - return std::make_shared(name, id); -} - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/server/rest_server.h b/third_party/gopher-orch/include/gopher/orch/server/rest_server.h deleted file mode 100644 index 4a35c9ed..00000000 --- a/third_party/gopher-orch/include/gopher/orch/server/rest_server.h +++ /dev/null @@ -1,333 +0,0 @@ -#pragma once - -// RESTServer - REST API implementation of Server interface -// -// Provides a Server implementation that wraps REST API endpoints as tools. -// Each tool maps to an HTTP endpoint with configurable method, path, and -// schema. -// -// Usage: -// RESTServerConfig config; -// config.name = "api-server"; -// config.base_url = "https://api.example.com/v1"; -// config.addTool("get_user", "GET", "/users/{id}", "Get user by ID"); -// config.addTool("create_user", "POST", "/users", "Create a new user"); -// -// auto server = RESTServer::create(config); -// auto getUserTool = server->tool("get_user"); -// -// Path parameters are substituted from the input JSON: -// /users/{id} with input {"id": "123"} becomes /users/123 - -#include -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/server/server.h" - -namespace gopher { -namespace orch { -namespace server { - -// Forward declarations -class RESTServer; -using RESTServerPtr = std::shared_ptr; - -// HTTP method enumeration -enum class HttpMethod { - GET, - POST, - PUT, - PATCH, - DELETE_, // DELETE is a macro on some platforms - HEAD, - OPTIONS -}; - -// Convert HttpMethod to string -inline std::string httpMethodToString(HttpMethod method) { - switch (method) { - case HttpMethod::GET: - return "GET"; - case HttpMethod::POST: - return "POST"; - case HttpMethod::PUT: - return "PUT"; - case HttpMethod::PATCH: - return "PATCH"; - case HttpMethod::DELETE_: - return "DELETE"; - case HttpMethod::HEAD: - return "HEAD"; - case HttpMethod::OPTIONS: - return "OPTIONS"; - default: - return "GET"; - } -} - -// Parse string to HttpMethod -inline HttpMethod parseHttpMethod(const std::string& method) { - if (method == "GET") - return HttpMethod::GET; - if (method == "POST") - return HttpMethod::POST; - if (method == "PUT") - return HttpMethod::PUT; - if (method == "PATCH") - return HttpMethod::PATCH; - if (method == "DELETE") - return HttpMethod::DELETE_; - if (method == "HEAD") - return HttpMethod::HEAD; - if (method == "OPTIONS") - return HttpMethod::OPTIONS; - return HttpMethod::GET; -} - -// Tool endpoint configuration -struct RESTToolEndpoint { - HttpMethod method = HttpMethod::GET; - std::string path; // e.g., "/users/{id}" - ServerToolInfo info; // Tool metadata - - // Request body handling - bool send_body = - true; // Send input JSON as request body (for POST/PUT/PATCH) - - // Response handling - std::string response_json_path; // JSONPath to extract from response (empty = - // use whole response) - - RESTToolEndpoint() = default; - RESTToolEndpoint(HttpMethod m, const std::string& p, const ServerToolInfo& i) - : method(m), - path(p), - info(i), - send_body(m != HttpMethod::GET && m != HttpMethod::DELETE_) {} -}; - -// Configuration for REST server connection -struct RESTServerConfig { - std::string name; // Human-readable name - std::string base_url; // Base URL (e.g., "https://api.example.com/v1") - - // Default headers for all requests - std::map default_headers; - - // Authentication - struct AuthConfig { - enum class Type { NONE, BEARER, BASIC, API_KEY }; - Type type = Type::NONE; - - std::string bearer_token; // For BEARER auth - std::string username; // For BASIC auth - std::string password; // For BASIC auth - std::string api_key; // For API_KEY auth - std::string api_key_header = "X-API-Key"; // Header name for API key - }; - AuthConfig auth; - - // Timeouts - std::chrono::milliseconds connect_timeout{10000}; - std::chrono::milliseconds request_timeout{30000}; - - // SSL/TLS - bool verify_ssl = true; - std::string ca_cert_path; // Optional CA certificate path - - // Tool endpoint mappings - std::map tools; - - // Fluent API for adding tools - RESTServerConfig& addTool(const std::string& name, - HttpMethod method, - const std::string& path, - const std::string& description = "") { - RESTToolEndpoint endpoint; - endpoint.method = method; - endpoint.path = path; - endpoint.info.name = name; - endpoint.info.description = description; - tools[name] = endpoint; - return *this; - } - - RESTServerConfig& addTool(const std::string& name, - const std::string& method, - const std::string& path, - const std::string& description = "") { - return addTool(name, parseHttpMethod(method), path, description); - } - - RESTServerConfig& setHeader(const std::string& name, - const std::string& value) { - default_headers[name] = value; - return *this; - } - - RESTServerConfig& setBearerAuth(const std::string& token) { - auth.type = AuthConfig::Type::BEARER; - auth.bearer_token = token; - return *this; - } - - RESTServerConfig& setBasicAuth(const std::string& username, - const std::string& password) { - auth.type = AuthConfig::Type::BASIC; - auth.username = username; - auth.password = password; - return *this; - } - - RESTServerConfig& setApiKey(const std::string& key, - const std::string& header = "X-API-Key") { - auth.type = AuthConfig::Type::API_KEY; - auth.api_key = key; - auth.api_key_header = header; - return *this; - } -}; - -// HTTP response from REST call -struct HttpResponse { - int status_code = 0; - std::map headers; - std::string body; - - bool isSuccess() const { return status_code >= 200 && status_code < 300; } - bool isClientError() const { return status_code >= 400 && status_code < 500; } - bool isServerError() const { return status_code >= 500; } -}; - -// HTTP client interface - abstraction for making HTTP requests -// This allows different implementations (libevent, curl, etc.) -class HttpClient { - public: - using ResponseCallback = std::function)>; - - virtual ~HttpClient() = default; - - // Make an HTTP request asynchronously - virtual void request(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - ResponseCallback callback) = 0; -}; - -using HttpClientPtr = std::shared_ptr; - -// RESTServer - REST API implementation of Server interface -// -// Thread Safety: -// - Configuration should be done before use -// - All public methods are thread-safe after configuration -// - Callbacks are invoked in dispatcher thread context -class RESTServer : public Server { - public: - using Ptr = std::shared_ptr; - - // Factory method - creates a REST server with default HTTP client - static Ptr create(const RESTServerConfig& config); - - // Factory method with custom HTTP client - static Ptr create(const RESTServerConfig& config, HttpClientPtr http_client); - - ~RESTServer() override; - - // Server interface implementation - std::string id() const override { return id_; } - std::string name() const override { return config_.name; } - ConnectionState connectionState() const override { return state_; } - - void connect(Dispatcher& dispatcher, ConnectionCallback callback) override; - void disconnect(Dispatcher& dispatcher, - std::function callback) override; - - void listTools(Dispatcher& dispatcher, - ServerToolListCallback callback) override; - - JsonRunnablePtr tool(const std::string& name) override; - - void callTool(const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) override; - - // REST-specific methods - - // Get the configuration - const RESTServerConfig& config() const { return config_; } - - // Update authentication at runtime - void setAuth(const RESTServerConfig::AuthConfig& auth); - - // Add a header that will be sent with all requests - void setDefaultHeader(const std::string& name, const std::string& value); - - private: - explicit RESTServer(const RESTServerConfig& config, - HttpClientPtr http_client); - - // Build full URL from endpoint path and arguments - std::string buildUrl(const std::string& path, const JsonValue& args) const; - - // Build request headers including auth - std::map buildHeaders() const; - - // Generate unique ID - static std::string generateId(); - - std::string id_; - RESTServerConfig config_; - HttpClientPtr http_client_; - ConnectionState state_ = ConnectionState::DISCONNECTED; - - // Cached tool runnables - std::map tool_cache_; - mutable std::mutex mutex_; -}; - -// Default HTTP client implementation using gopher-mcp networking -// Note: This is a basic implementation. For production use, consider -// using a more robust HTTP client library. -class DefaultHttpClient : public HttpClient { - public: - DefaultHttpClient(); - ~DefaultHttpClient() override; - - void request(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - ResponseCallback callback) override; - - private: - class Impl; - std::unique_ptr impl_; -}; - -// Factory function -inline RESTServerPtr makeRESTServer(const RESTServerConfig& config) { - return RESTServer::create(config); -} - -// Factory for CurlHttpClient implementation -std::shared_ptr createCurlHttpClient(); - -// Utility function for making synchronous JSON HTTP GET requests using CurlHttpClient -std::string fetchJsonSync(const std::string& url, - const std::map& headers = {}); - - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/server/server.h b/third_party/gopher-orch/include/gopher/orch/server/server.h deleted file mode 100644 index 498fa9ca..00000000 --- a/third_party/gopher-orch/include/gopher/orch/server/server.h +++ /dev/null @@ -1,142 +0,0 @@ -#pragma once - -// Server - Protocol-agnostic server abstraction -// -// Defines a common interface for interacting with tool-providing servers -// regardless of the underlying protocol (MCP, REST, gRPC, mock, etc.) -// -// Key abstractions: -// - Server: Connection to a tool provider -// - ServerTool: A tool exposed by the server (implements Runnable) -// - ServerToolInfo: Metadata about a tool from a server - -#include -#include -#include -#include -#include - -#include "gopher/orch/core/runnable.h" - -namespace gopher { -namespace orch { -namespace server { - -using namespace gopher::orch::core; - -// Forward declarations -class Server; -class ServerTool; - -using ServerPtr = std::shared_ptr; -using ServerToolPtr = std::shared_ptr; - -// Information about a tool exposed by a server -struct ServerToolInfo { - std::string name; - std::string description; - JsonValue inputSchema; // JSON Schema for tool arguments - - ServerToolInfo() = default; - ServerToolInfo(const std::string& n, const std::string& desc = "") - : name(n), description(desc), inputSchema(JsonValue::object()) {} -}; - -// Connection state for server -enum class ConnectionState { - DISCONNECTED, - CONNECTING, - CONNECTED, - RECONNECTING, - FAILED -}; - -// Callback types -using ConnectionCallback = std::function)>; -using ServerToolListCallback = - std::function>)>; - -// Server - Abstract interface for protocol-agnostic server access -// -// Implementations: -// - MockServer: For testing without network -// - MCPServer: For MCP protocol (stdio, SSE, WebSocket) -// - RESTServer: For REST API endpoints -class Server : public std::enable_shared_from_this { - public: - virtual ~Server() = default; - - // Unique identifier for this server instance - virtual std::string id() const = 0; - - // Human-readable name - virtual std::string name() const = 0; - - // Current connection state - virtual ConnectionState connectionState() const = 0; - - // Check if connected - bool isConnected() const { - return connectionState() == ConnectionState::CONNECTED; - } - - // Connect to the server (async) - // Callback invoked in dispatcher context when connection completes or fails - virtual void connect(Dispatcher& dispatcher, ConnectionCallback callback) = 0; - - // Disconnect from the server (async) - virtual void disconnect(Dispatcher& dispatcher, - std::function callback = nullptr) = 0; - - // List available tools (async) - // May return cached list if already connected - virtual void listTools(Dispatcher& dispatcher, - ServerToolListCallback callback) = 0; - - // Get a tool by name as a Runnable - // Returns nullptr if tool not found - virtual JsonRunnablePtr tool(const std::string& name) = 0; - - // Call a tool directly (convenience method) - // Equivalent to tool(name)->invoke(...) - virtual void callTool(const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) = 0; - - // Get shared pointer to this server - ServerPtr shared() { return shared_from_this(); } - - protected: - Server() = default; -}; - -// ServerTool - A tool exposed by a server, implements Runnable -// -// Wraps a tool call through the server's protocol -class ServerTool : public JsonRunnable { - public: - ServerTool(ServerPtr server, const ServerToolInfo& info) - : server_(std::move(server)), info_(info) {} - - std::string name() const override { return info_.name; } - - const ServerToolInfo& info() const { return info_; } - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - server_->callTool(info_.name, input, config, dispatcher, - std::move(callback)); - } - - private: - ServerPtr server_; - ServerToolInfo info_; -}; - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/gopher/orch/server/server_composite.h b/third_party/gopher-orch/include/gopher/orch/server/server_composite.h deleted file mode 100644 index baf4ac28..00000000 --- a/third_party/gopher-orch/include/gopher/orch/server/server_composite.h +++ /dev/null @@ -1,412 +0,0 @@ -#pragma once - -// ServerComposite - Aggregate tools from multiple servers -// -// Provides a unified view of tools from multiple servers, regardless of -// their underlying protocol (MCP, REST, Mock, etc.). -// -// Features: -// - Namespace tools by server name to avoid conflicts -// - Support tool aliasing for cleaner API -// - Lazy connection: servers connect when their tools are first used -// - Tool discovery across all registered servers -// -// Usage: -// auto composite = ServerComposite::create("my-tools"); -// composite->addServer(mcp_server); -// composite->addServer(rest_server); -// -// // Get tool with automatic server routing -// auto tool = composite->tool("weather.get_forecast"); -// -// // Or with explicit server name -// auto tool = composite->tool("mcp-server", "get_forecast"); - -#include -#include -#include -#include - -#include "gopher/orch/server/server.h" - -namespace gopher { -namespace orch { -namespace server { - -// Forward declaration -class ServerComposite; -using ServerCompositePtr = std::shared_ptr; - -// Configuration for how tools are exposed -struct ToolMapping { - std::string server_name; // Source server - std::string tool_name; // Tool name on server - std::string alias; // Exposed name (empty = use tool_name) - - ToolMapping() = default; - ToolMapping(const std::string& server, - const std::string& tool, - const std::string& alias_name = "") - : server_name(server), tool_name(tool), alias(alias_name) {} -}; - -// ServerComposite - Aggregates tools from multiple servers -// -// This class provides a unified interface to tools from multiple servers. -// Tools can be accessed either by their fully-qualified name (server.tool) -// or by alias if configured. -// -// Thread Safety: -// - Thread-safe for read operations (listTools, tool) -// - Not thread-safe for write operations (addServer, addTool) -// - Write operations should be done during initialization -class ServerComposite : public std::enable_shared_from_this { - public: - using Ptr = std::shared_ptr; - - // Create a new ServerComposite - static Ptr create(const std::string& name) { - return std::shared_ptr(new ServerComposite(name)); - } - - // Get the composite name - const std::string& name() const { return name_; } - - // Add a server and expose all its tools - // Tools are namespaced as "server_name.tool_name" - // If namespace_tools is false, tools are exposed without prefix - ServerComposite& addServer(ServerPtr server, bool namespace_tools = true); - - // Add a server with specific tools only - ServerComposite& addServer(ServerPtr server, - const std::vector& tool_names, - bool namespace_tools = true); - - // Add a server with tool aliases - ServerComposite& addServerWithAliases( - ServerPtr server, const std::map& aliases); - - // Add a specific tool with optional alias - ServerComposite& addTool(ServerPtr server, - const std::string& tool_name, - const std::string& alias = ""); - - // Remove a server and all its tools - void removeServer(const std::string& server_name); - - // Get a tool by name - // Supports: - // - Fully-qualified name: "server_name.tool_name" - // - Alias: "my_alias" - // - Direct name if unique: "tool_name" - JsonRunnablePtr tool(const std::string& name); - - // Get a tool by server and tool name - JsonRunnablePtr tool(const std::string& server_name, - const std::string& tool_name); - - // List all available tools (with their exposed names) - std::vector listTools() const; - - // List all available tools with full info - std::vector listToolInfos() const; - - // Get all registered servers - const std::map& servers() const { return servers_; } - - // Get server by name - ServerPtr server(const std::string& name) const; - - // Check if a tool exists - bool hasTool(const std::string& name) const; - - // Connect all servers - // Calls connect() on each server and invokes callback when all complete - void connectAll(Dispatcher& dispatcher, - std::function)> callback); - - // Disconnect all servers - void disconnectAll(Dispatcher& dispatcher, std::function callback); - - private: - explicit ServerComposite(const std::string& name) : name_(name) {} - - // Resolve tool name to server and actual tool name - // Returns {server_ptr, tool_name} or {nullptr, ""} if not found - std::pair resolveToolName( - const std::string& name) const; - - std::string name_; - std::map servers_; - - // Tool mappings: exposed_name -> {server_name, actual_tool_name} - std::map> tool_mappings_; - - // Cached tool runnables - mutable std::map tool_cache_; -}; - -// CompositeServerTool - A tool that routes through ServerComposite -// -// This wrapper handles tool resolution and caching at the composite level. -class CompositeServerTool : public JsonRunnable { - public: - CompositeServerTool(ServerCompositePtr composite, - const std::string& exposed_name, - ServerPtr server, - const std::string& tool_name) - : composite_(std::move(composite)), - exposed_name_(exposed_name), - server_(std::move(server)), - tool_name_(tool_name) {} - - std::string name() const override { return exposed_name_; } - - void invoke(const JsonValue& input, - const RunnableConfig& config, - Dispatcher& dispatcher, - Callback callback) override { - server_->callTool(tool_name_, input, config, dispatcher, - std::move(callback)); - } - - private: - ServerCompositePtr composite_; - std::string exposed_name_; - ServerPtr server_; - std::string tool_name_; -}; - -// Implementation - -inline ServerComposite& ServerComposite::addServer(ServerPtr server, - bool namespace_tools) { - std::string server_name = server->name(); - servers_[server_name] = server; - - // We can't list tools synchronously here since server might not be connected - // Instead, we mark that we need to discover tools lazily - // For now, assume tools are already known (via listTools cache) - - return *this; -} - -inline ServerComposite& ServerComposite::addServer( - ServerPtr server, - const std::vector& tool_names, - bool namespace_tools) { - std::string server_name = server->name(); - servers_[server_name] = server; - - for (const auto& tool_name : tool_names) { - std::string exposed = - namespace_tools ? server_name + "." + tool_name : tool_name; - tool_mappings_[exposed] = {server_name, tool_name}; - } - - return *this; -} - -inline ServerComposite& ServerComposite::addServerWithAliases( - ServerPtr server, const std::map& aliases) { - std::string server_name = server->name(); - servers_[server_name] = server; - - for (const auto& entry : aliases) { - // entry.first = alias, entry.second = tool_name - tool_mappings_[entry.first] = {server_name, entry.second}; - } - - return *this; -} - -inline ServerComposite& ServerComposite::addTool(ServerPtr server, - const std::string& tool_name, - const std::string& alias) { - std::string server_name = server->name(); - servers_[server_name] = server; - - std::string exposed = alias.empty() ? tool_name : alias; - tool_mappings_[exposed] = {server_name, tool_name}; - - return *this; -} - -inline void ServerComposite::removeServer(const std::string& server_name) { - servers_.erase(server_name); - - // Remove tool mappings for this server - auto it = tool_mappings_.begin(); - while (it != tool_mappings_.end()) { - if (it->second.first == server_name) { - // Also remove from cache - tool_cache_.erase(it->first); - it = tool_mappings_.erase(it); - } else { - ++it; - } - } -} - -inline std::pair ServerComposite::resolveToolName( - const std::string& name) const { - // First check explicit mappings - auto mapping_it = tool_mappings_.find(name); - if (mapping_it != tool_mappings_.end()) { - auto server_it = servers_.find(mapping_it->second.first); - if (server_it != servers_.end()) { - return {server_it->second, mapping_it->second.second}; - } - } - - // Check for fully-qualified name (server.tool) - auto dot_pos = name.find('.'); - if (dot_pos != std::string::npos) { - std::string server_name = name.substr(0, dot_pos); - std::string tool_name = name.substr(dot_pos + 1); - - auto server_it = servers_.find(server_name); - if (server_it != servers_.end()) { - return {server_it->second, tool_name}; - } - } - - // Try each server for a direct tool name match - for (const auto& entry : servers_) { - // This would require checking if the server has this tool - // For now, we return the first server that might have it - // A proper implementation would check tool availability - } - - return {nullptr, ""}; -} - -inline JsonRunnablePtr ServerComposite::tool(const std::string& name) { - // Check cache - auto cache_it = tool_cache_.find(name); - if (cache_it != tool_cache_.end()) { - return cache_it->second; - } - - // Resolve and create - auto resolved = resolveToolName(name); - if (!resolved.first) { - return nullptr; - } - - auto tool_ptr = std::make_shared( - std::const_pointer_cast( - std::static_pointer_cast(shared_from_this())), - name, resolved.first, resolved.second); - - tool_cache_[name] = tool_ptr; - return tool_ptr; -} - -inline JsonRunnablePtr ServerComposite::tool(const std::string& server_name, - const std::string& tool_name) { - std::string full_name = server_name + "." + tool_name; - return tool(full_name); -} - -inline std::vector ServerComposite::listTools() const { - std::vector result; - result.reserve(tool_mappings_.size()); - - for (const auto& entry : tool_mappings_) { - result.push_back(entry.first); - } - - return result; -} - -inline std::vector ServerComposite::listToolInfos() const { - std::vector result; - - for (const auto& entry : tool_mappings_) { - ServerToolInfo info; - info.name = entry.first; - - // Try to get description from server - auto server_it = servers_.find(entry.second.first); - if (server_it != servers_.end()) { - // Would need to query server for tool info - // For now, leave description empty - } - - result.push_back(info); - } - - return result; -} - -inline ServerPtr ServerComposite::server(const std::string& name) const { - auto it = servers_.find(name); - return it != servers_.end() ? it->second : nullptr; -} - -inline bool ServerComposite::hasTool(const std::string& name) const { - return resolveToolName(name).first != nullptr; -} - -inline void ServerComposite::connectAll( - Dispatcher& dispatcher, - std::function)> callback) { - if (servers_.empty()) { - dispatcher.post( - [callback]() { callback(core::makeSuccess(nullptr)); }); - return; - } - - // Track connection results - auto pending = std::make_shared>(servers_.size()); - auto has_error = std::make_shared>(false); - auto first_error = std::make_shared(); - - for (const auto& entry : servers_) { - entry.second->connect(dispatcher, [pending, has_error, first_error, - callback, &dispatcher]( - Result result) { - if (core::isError(result) && !has_error->exchange(true)) { - *first_error = core::getError(result); - } - - if (--(*pending) == 0) { - // All servers done - dispatcher.post([callback, has_error, first_error]() { - if (*has_error) { - callback(Result(*first_error)); - } else { - callback(core::makeSuccess(nullptr)); - } - }); - } - }); - } -} - -inline void ServerComposite::disconnectAll(Dispatcher& dispatcher, - std::function callback) { - if (servers_.empty()) { - if (callback) { - dispatcher.post(callback); - } - return; - } - - auto pending = std::make_shared>(servers_.size()); - - for (const auto& entry : servers_) { - entry.second->disconnect(dispatcher, [pending, callback, &dispatcher]() { - if (--(*pending) == 0) { - if (callback) { - dispatcher.post(callback); - } - } - }); - } -} - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/orch/core/hello.h b/third_party/gopher-orch/include/orch/core/hello.h deleted file mode 100644 index 48c6cdc7..00000000 --- a/third_party/gopher-orch/include/orch/core/hello.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#include - -namespace gopher { -namespace orch { -namespace core { - -// Hello class demonstrates basic gopher-orch functionality -// This is a simple example to verify the build system works correctly -class Hello { - public: - Hello(); - explicit Hello(const std::string& name); - ~Hello(); - - std::string greet() const; - std::string greet_with_prefix(const std::string& prefix) const; - - void set_name(const std::string& name); - const std::string& get_name() const; - - static std::string get_version(); - - private: - class Impl; - std::unique_ptr impl_; -}; - -// Builder pattern for Hello class construction -class HelloBuilder { - public: - HelloBuilder& with_name(const std::string& name); - HelloBuilder& with_greeting_style(const std::string& style); - - std::unique_ptr build() const; - - private: - std::string name_ = "World"; - std::string style_ = "default"; -}; - -} // namespace core -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/include/orch/core/version.h b/third_party/gopher-orch/include/orch/core/version.h deleted file mode 100644 index d86166e9..00000000 --- a/third_party/gopher-orch/include/orch/core/version.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#define GOPHER_ORCH_VERSION_MAJOR 0 -#define GOPHER_ORCH_VERSION_MINOR 1 -#define GOPHER_ORCH_VERSION_PATCH 0 - -#define GOPHER_ORCH_VERSION_STRING "0.1.0" - -namespace gopher { -namespace orch { -namespace core { - -struct Version { - static constexpr int major() { return GOPHER_ORCH_VERSION_MAJOR; } - static constexpr int minor() { return GOPHER_ORCH_VERSION_MINOR; } - static constexpr int patch() { return GOPHER_ORCH_VERSION_PATCH; } - static const char* string() { return GOPHER_ORCH_VERSION_STRING; } -}; - -} // namespace core -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/sdk/typescript/README.md b/third_party/gopher-orch/sdk/typescript/README.md deleted file mode 100644 index c2b95963..00000000 --- a/third_party/gopher-orch/sdk/typescript/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# Gopher-Orch TypeScript SDK - -TypeScript SDK for the gopher-orch orchestration framework, providing agent capabilities through FFI bindings. - -## Features - -- **ReActAgent**: AI agent with reasoning and acting capabilities -- **Remote API Integration**: Fetch server configurations from remote APIs -- **Local Configuration**: Use JSON-based server configurations -- **Error Handling**: Comprehensive error types and handling -- **TypeScript Support**: Full type safety and IntelliSense support - -## Installation - -```bash -npm install gopher-orch-sdk -``` - -## Quick Start - -### Using Local JSON Configuration - -```typescript -import { ReActAgent, ServerConfigHelper } from 'gopher-orch-sdk'; - -// Create agent with local server configuration -const serverConfig = ServerConfigHelper.createDefaultConfig(); -const agent = ReActAgent.createByJson('AnthropicProvider', 'claude-3-haiku-20240307', serverConfig); - -// Run a query -const response = agent.run('What tools are available?'); -console.log(response); - -// Clean up -agent.dispose(); -``` - -### Using Remote API - -```typescript -import { ReActAgent } from 'gopher-orch-sdk'; - -// Create agent that fetches configuration from remote API -const agent = ReActAgent.createByApiKey('AnthropicProvider', 'claude-3-haiku-20240307', 'your-api-key'); - -// Run queries -const response1 = agent.run('What time is it in Tokyo?'); -const response2 = agent.run('Generate a secure password'); - -console.log('Response 1:', response1); -console.log('Response 2:', response2); - -// Clean up -agent.dispose(); -``` - -## API Reference - -### ReActAgent - -#### Static Methods - -- `createByJson(provider: string, model: string, serverConfig: string): ReActAgent | null` - - Create agent from JSON server configuration - -- `createByApiKey(provider: string, model: string, apiKey: string): ReActAgent | null` - - Create agent by fetching configuration from remote API - -#### Instance Methods - -- `run(query: string, timeoutMs?: number): string` - - Execute a query synchronously - - Default timeout: 60 seconds - -- `runDetailed(query: string, timeoutMs?: number): AgentResult` - - Execute query with detailed result information - -- `dispose(): void` - - Clean up agent resources - -- `isDisposed(): boolean` - - Check if agent has been disposed - -### Error Types - -- `AgentError`: Base error class for all agent-related errors -- `ApiKeyError`: Invalid or missing API key -- `ConnectionError`: Failed to connect to MCP servers -- `TimeoutError`: Agent execution timed out - -### ServerConfigHelper - -- `fetchMcpServers(apiKey: string): Promise` - - Fetch server configurations from remote API - -- `createDefaultConfig(): string` - - Create default local server configuration for development - -## Examples - -See the `/examples/sdk/typescript/` directory for complete examples: - -- `clientExampleSimple.ts` - Basic usage with local configuration -- `clientExampleApi.ts` - Remote API integration - -## Requirements - -- Node.js 16+ -- The gopher-orch native library must be available -- For remote API features: valid API key and network connection - -## Building - -```bash -npm run build -``` - -## License - -MIT \ No newline at end of file diff --git a/third_party/gopher-orch/sdk/typescript/package-lock.json b/third_party/gopher-orch/sdk/typescript/package-lock.json deleted file mode 100644 index 46563466..00000000 --- a/third_party/gopher-orch/sdk/typescript/package-lock.json +++ /dev/null @@ -1,3690 +0,0 @@ -{ - "name": "gopher-orch-sdk", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "gopher-orch-sdk", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "koffi": "^2.8.0" - }, - "devDependencies": { - "@types/jest": "^29.0.0", - "@types/node": "^18.0.0", - "jest": "^29.0.0", - "typescript": "^5.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", - "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001763", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", - "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/koffi": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.0.tgz", - "integrity": "sha512-174BTuWK7L42Om7nDWy9YOTXj6Dkm14veuFf5yhVS5VU6GjtOI1Wjf+K16Z0JvSuZ3/NpkVzFBjE1oKbthTIEA==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "url": "https://buymeacoffee.com/koromix" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/third_party/gopher-orch/sdk/typescript/package.json b/third_party/gopher-orch/sdk/typescript/package.json deleted file mode 100644 index 2ec2f1e1..00000000 --- a/third_party/gopher-orch/sdk/typescript/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "gopher-orch-sdk", - "version": "1.0.0", - "description": "TypeScript SDK for gopher-orch orchestration framework", - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "test": "jest", - "clean": "rm -rf dist" - }, - "dependencies": { - "koffi": "^2.8.0" - }, - "devDependencies": { - "@types/node": "^18.0.0", - "typescript": "^5.0.0", - "jest": "^29.0.0", - "@types/jest": "^29.0.0" - }, - "keywords": [ - "orchestration", - "agent", - "ai", - "mcp", - "typescript" - ], - "author": "Gopher Security", - "license": "MIT" -} \ No newline at end of file diff --git a/third_party/gopher-orch/sdk/typescript/src/agent.ts b/third_party/gopher-orch/sdk/typescript/src/agent.ts deleted file mode 100644 index d18f8eca..00000000 --- a/third_party/gopher-orch/sdk/typescript/src/agent.ts +++ /dev/null @@ -1,346 +0,0 @@ -/** - * @file agent.ts - * @brief TypeScript wrapper for GopherAgent functionality - */ - -import { library, initializeLibrary, shutdownLibrary, getLastError, clearError } from './ffi.js'; -import { - AgentConfig, - AgentResult, - AgentError, - ApiKeyError, - ConnectionError, - TimeoutError, - ApiResponse -} from './types.js'; - -/** - * Configuration options for creating a GopherAgent - */ -export interface GopherAgentConfig { - /** Provider name (e.g., "AnthropicProvider") */ - provider: string; - /** Model name (e.g., "claude-3-haiku-20240307") */ - model: string; - /** API key for fetching remote server config (mutually exclusive with serverConfig) */ - apiKey?: string; - /** JSON server configuration (mutually exclusive with apiKey) */ - serverConfig?: string; -} - -/** - * GopherAgent - Main entry point for the gopher-orch TypeScript SDK - * - * Provides a clean, TypeScript-friendly interface to the gopher-orch agent functionality. - * - * @example - * ```typescript - * import { GopherAgent } from "gopher-orch-sdk"; - * - * // Initialize the library - * GopherAgent.init(); - * - * // Create an agent with API key - * const agent = GopherAgent.create({ - * provider: 'AnthropicProvider', - * model: 'claude-3-haiku-20240307', - * apiKey: 'your-api-key' - * }); - * - * // Run a query - * const answer = agent.run("What time is it in Tokyo?"); - * console.log(answer); - * - * // Cleanup (optional - happens automatically on exit) - * agent.dispose(); - * ``` - */ -export class GopherAgent { - private handle: any; - private disposed: boolean = false; - private static initialized: boolean = false; - - private constructor(handle: any) { - this.handle = handle; - } - - /** - * Initialize the gopher-orch library - * Must be called before creating any agents - * - * @throws {AgentError} If initialization fails - */ - static init(): void { - if (GopherAgent.initialized) { - return; - } - - const success = initializeLibrary(); - if (!success) { - throw new AgentError('Failed to initialize gopher-orch library'); - } - - library.gopher_orch_init(); - GopherAgent.initialized = true; - - // Setup automatic cleanup on process exit - GopherAgent.setupCleanupHandlers(); - } - - /** - * Shutdown the gopher-orch library - * Called automatically on process exit, but can be called manually - */ - static shutdown(): void { - if (GopherAgent.initialized) { - shutdownLibrary(); - GopherAgent.initialized = false; - } - } - - /** - * Check if the library is initialized - */ - static isInitialized(): boolean { - return GopherAgent.initialized; - } - - /** - * Create a new GopherAgent instance - * - * @param config Configuration options - * @returns GopherAgent instance - * @throws {AgentError} If agent creation fails - * - * @example - * ```typescript - * // Create with API key (fetches server config from remote API) - * const agent = GopherAgent.create({ - * provider: 'AnthropicProvider', - * model: 'claude-3-haiku-20240307', - * apiKey: 'your-api-key' - * }); - * - * // Or create with JSON server config - * const agent = GopherAgent.create({ - * provider: 'AnthropicProvider', - * model: 'claude-3-haiku-20240307', - * serverConfig: '{"succeeded": true, "data": {...}}' - * }); - * ``` - */ - static create(config: GopherAgentConfig): GopherAgent { - if (!GopherAgent.initialized) { - GopherAgent.init(); - } - - const { provider, model, apiKey, serverConfig } = config; - - if (!provider || !model) { - throw new AgentError('Provider and model are required'); - } - - if (apiKey && serverConfig) { - throw new AgentError('Cannot specify both apiKey and serverConfig'); - } - - if (!apiKey && !serverConfig) { - throw new AgentError('Either apiKey or serverConfig is required'); - } - - let handle: any; - - try { - if (apiKey) { - handle = library.gopher_orch_agent_create_by_api_key(provider, model, apiKey); - } else { - handle = library.gopher_orch_agent_create_by_json(provider, model, serverConfig!); - } - - if (!handle || (handle.isNull && handle.isNull())) { - // Try to get error message from FFI layer - const lastError = library.gopher_orch_last_error(); - const errorMsg = lastError ? String(lastError) : 'Failed to create agent'; - library.gopher_orch_clear_error(); - throw new AgentError(errorMsg); - } - - return new GopherAgent(handle); - } catch (error: any) { - if (error instanceof AgentError) { - throw error; - } - throw new AgentError(`Failed to create agent: ${error.message}`); - } - } - - /** - * Run a query against the agent - * - * @param query The user query to process - * @param timeoutMs Optional timeout in milliseconds (default: 60000) - * @returns The agent's response - * @throws {AgentError} If the query fails - */ - run(query: string, timeoutMs: number = 60000): string { - this.ensureNotDisposed(); - - try { - const response = library.gopher_orch_agent_run(this.handle, query, timeoutMs); - return response; - } catch (error: any) { - throw new AgentError(`Query execution failed: ${error.message}`); - } - } - - /** - * Run a query with detailed result information - * - * @param query The user query to process - * @param timeoutMs Optional timeout in milliseconds - * @returns AgentResult with response and metadata - */ - runDetailed(query: string, timeoutMs: number = 60000): AgentResult { - try { - const response = this.run(query, timeoutMs); - - return { - response, - status: 'success', - iterationCount: 1, - tokensUsed: 0, - }; - } catch (error: any) { - if (error instanceof TimeoutError) { - return { - response: error.message, - status: 'timeout' - }; - } else { - return { - response: error.message, - status: 'error' - }; - } - } - } - - /** - * Dispose of the agent and free resources - */ - dispose(): void { - if (!this.disposed) { - if (this.handle) { - library.gopher_orch_agent_release(this.handle); - this.handle = null; - } - this.disposed = true; - } - } - - /** - * Check if agent is disposed - */ - isDisposed(): boolean { - return this.disposed; - } - - private ensureNotDisposed(): void { - if (this.disposed) { - throw new AgentError('Agent has been disposed'); - } - } - - private static setupCleanupHandlers(): void { - const cleanup = () => { - GopherAgent.shutdown(); - }; - - process.on('exit', cleanup); - process.on('SIGTERM', () => { - cleanup(); - process.exit(0); - }); - process.on('SIGINT', () => { - cleanup(); - process.exit(0); - }); - } -} - -// Keep ReActAgent as an alias for backward compatibility -export { GopherAgent as ReActAgent }; - -/** - * Utility functions for working with server configurations - */ -export class ServerConfig { - /** - * Fetch MCP server configurations from remote API - * - * @param apiKey API key for authentication - * @returns Server configuration JSON string - */ - static fetch(apiKey: string): string { - if (!GopherAgent.isInitialized()) { - GopherAgent.init(); - } - - try { - if (!apiKey || apiKey.trim().length === 0) { - throw new ApiKeyError('Invalid or missing API key'); - } - - return library.gopher_orch_api_fetch_servers(apiKey); - } catch (error: any) { - if (error instanceof AgentError) { - throw error; - } - throw new AgentError(`Failed to fetch servers: ${error.message}`); - } - } - - /** - * Create default server configuration for local development - */ - static createDefault(): string { - const defaultConfig: ApiResponse = { - succeeded: true, - code: 200000000, - message: "success", - data: { - servers: [ - { - version: "2025-01-09", - serverId: "1877234567890123456", - name: "local-dev-server", - transport: "http_sse", - config: { - url: "http://127.0.0.1:3001/rpc", - headers: {} - }, - connectTimeout: 5000, - requestTimeout: 30000 - }, - { - version: "2025-01-09", - serverId: "1877234567890123457", - name: "local-dev-server2", - transport: "http_sse", - config: { - url: "http://127.0.0.1:3002/rpc", - headers: {} - }, - connectTimeout: 5000, - requestTimeout: 30000 - } - ] - } - }; - - return JSON.stringify(defaultConfig); - } -} - -// Keep ServerConfigHelper as an alias for backward compatibility -export { ServerConfig as ServerConfigHelper }; diff --git a/third_party/gopher-orch/sdk/typescript/src/ffi.ts b/third_party/gopher-orch/sdk/typescript/src/ffi.ts deleted file mode 100644 index 139b47c1..00000000 --- a/third_party/gopher-orch/sdk/typescript/src/ffi.ts +++ /dev/null @@ -1,383 +0,0 @@ -/** - * @file ffi.ts - * @brief Real FFI interface to gopher-orch C++ library using koffi - */ - -import { existsSync } from "node:fs"; -import koffi from "koffi"; -import { arch, platform } from "node:os"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -// ESM equivalent of __dirname -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Library configuration for different platforms and architectures -const LIBRARY_CONFIG = { - darwin: { - x64: { - name: "libgopher-orch.dylib", - searchPaths: [ - // Development build path (relative to this file) - join(__dirname, "../../../build/lib/libgopher-orch.dylib"), - join(__dirname, "../../../../build/lib/libgopher-orch.dylib"), - // Example local lib - join(__dirname, "../../examples/sdk/typescript/lib/libgopher-orch.dylib"), - // System installation paths - "/usr/local/lib/libgopher-orch.dylib", - "/opt/homebrew/lib/libgopher-orch.dylib", - "/usr/lib/libgopher-orch.dylib", - ], - }, - arm64: { - name: "libgopher-orch.dylib", - searchPaths: [ - // Development build path (relative to this file) - join(__dirname, "../../../build/lib/libgopher-orch.dylib"), - join(__dirname, "../../../../build/lib/libgopher-orch.dylib"), - // Example local lib - join(__dirname, "../../examples/sdk/typescript/lib/libgopher-orch.dylib"), - // System installation paths - "/usr/local/lib/libgopher-orch.dylib", - "/opt/homebrew/lib/libgopher-orch.dylib", - "/usr/lib/libgopher-orch.dylib", - ], - }, - }, - linux: { - x64: { - name: "libgopher-orch.so", - searchPaths: [ - join(__dirname, "../../../build/lib/libgopher-orch.so"), - join(__dirname, "../../../../build/lib/libgopher-orch.so"), - join(__dirname, "../../examples/sdk/typescript/lib/libgopher-orch.so"), - "/usr/local/lib/libgopher-orch.so", - "/usr/lib/x86_64-linux-gnu/libgopher-orch.so", - "/usr/lib64/libgopher-orch.so", - "/usr/lib/libgopher-orch.so", - ], - }, - arm64: { - name: "libgopher-orch.so", - searchPaths: [ - join(__dirname, "../../../build/lib/libgopher-orch.so"), - join(__dirname, "../../../../build/lib/libgopher-orch.so"), - join(__dirname, "../../examples/sdk/typescript/lib/libgopher-orch.so"), - "/usr/local/lib/libgopher-orch.so", - "/usr/lib/aarch64-linux-gnu/libgopher-orch.so", - "/usr/lib64/libgopher-orch.so", - "/usr/lib/libgopher-orch.so", - ], - }, - }, - win32: { - x64: { - name: "gopher-orch.dll", - searchPaths: [ - join(__dirname, "../../../build/lib/gopher-orch.dll"), - join(__dirname, "../../../../build/lib/gopher-orch.dll"), - "C:\\Program Files\\gopher-orch\\bin\\gopher-orch.dll", - "C:\\Program Files\\gopher-orch\\lib\\gopher-orch.dll", - ], - }, - }, -} as const; - -function getLibraryPath(): string { - // Check for environment variable override first - const envPath = process.env["GOPHER_ORCH_LIBRARY_PATH"]; - if (envPath && existsSync(envPath)) { - return envPath; - } - - const currentPlatform = platform() as keyof typeof LIBRARY_CONFIG; - const currentArch = arch() as keyof (typeof LIBRARY_CONFIG)[typeof currentPlatform]; - - if (!LIBRARY_CONFIG[currentPlatform] || !LIBRARY_CONFIG[currentPlatform][currentArch]) { - throw new Error(`Unsupported platform: ${currentPlatform} ${currentArch}`); - } - - const config = LIBRARY_CONFIG[currentPlatform][currentArch]; - - // Search through the paths to find the first one that exists - for (const searchPath of config.searchPaths) { - if (existsSync(searchPath)) { - return searchPath; - } - } - - // If no path found, throw an error with helpful information - const searchedPaths = config.searchPaths.join(", "); - throw new Error( - `Gopher-Orch library not found. Searched paths: ${searchedPaths}\n` + - `Please ensure the library is built and available at one of these locations.\n` + - `You can set GOPHER_ORCH_LIBRARY_PATH environment variable to the library path.` - ); -} - -// Gopher-Orch Library interface - using real C API functions -export let gopherOrchLib: any = {}; - -// Define the error info struct type for koffi -let ErrorInfoType: any = null; - -try { - const libPath = getLibraryPath(); - - // Load the shared library - const library = koffi.load(libPath); - - // Define the error_info struct to match C: - // typedef struct { - // gopher_orch_error_t code; // int - // const char* message; - // const char* details; - // const char* file; - // int32_t line; - // } gopher_orch_error_info_t; - ErrorInfoType = koffi.struct('gopher_orch_error_info_t', { - code: 'int', - message: 'const char*', - details: 'const char*', - file: 'const char*', - line: 'int32_t' - }); - - // Try to bind functions and see which ones are available - const availableFunctions: any = {}; - - // List of C API functions from orch_ffi.h - const functionList = [ - // Core initialization functions - { name: "gopher_orch_init", signature: "int", args: [] }, - { name: "gopher_orch_shutdown", signature: "void", args: [] }, - { name: "gopher_orch_is_initialized", signature: "int", args: [] }, - { name: "gopher_orch_last_error", signature: "gopher_orch_error_info_t*", args: [] }, - { name: "gopher_orch_clear_error", signature: "void", args: [] }, - { name: "gopher_orch_free", signature: "void", args: ["void*"] }, - - // Agent functions - { - name: "gopher_orch_agent_create_by_json", - signature: "void*", // gopher_orch_agent_t - args: ["string", "string", "string"], // provider, model, server_json_config - }, - { - name: "gopher_orch_agent_create_by_api_key", - signature: "void*", // gopher_orch_agent_t - args: ["string", "string", "string"], // provider, model, api_key - }, - { - name: "gopher_orch_agent_run", - signature: "string", // Returns string (OWNED - caller must gopher_orch_free) - args: ["void*", "string", "uint64_t"], // agent, query, timeout_ms - }, - { name: "gopher_orch_agent_add_ref", signature: "void", args: ["void*"] }, - { name: "gopher_orch_agent_release", signature: "void", args: ["void*"] }, - - // API functions - { - name: "gopher_orch_api_fetch_servers", - signature: "string", // Returns JSON string (OWNED - caller must gopher_orch_free) - args: ["string"], // api_key - }, - ]; - - // Try to bind each function - for (const func of functionList) { - try { - availableFunctions[func.name] = library.func(func.name, func.signature, func.args); - } catch (error: any) { - // Function not available - that's OK, we'll provide a fallback - } - } - - gopherOrchLib = availableFunctions; - -} catch (error) { - console.error(`Failed to load Gopher-Orch library: ${error}`); - // Continue with empty lib - fallbacks will be used - gopherOrchLib = {}; -} - -// FFI interface with fallbacks -export const library = { - gopher_orch_init: () => { - if (gopherOrchLib.gopher_orch_init) { - return gopherOrchLib.gopher_orch_init(); - } - return 0; // Success fallback - }, - - gopher_orch_shutdown: () => { - if (gopherOrchLib.gopher_orch_shutdown) { - gopherOrchLib.gopher_orch_shutdown(); - } - }, - - gopher_orch_is_initialized: () => { - if (gopherOrchLib.gopher_orch_is_initialized) { - return gopherOrchLib.gopher_orch_is_initialized() !== 0; - } - return true; // Fallback - }, - - gopher_orch_last_error: (): string | null => { - if (gopherOrchLib.gopher_orch_last_error && ErrorInfoType) { - try { - const errorPtr = gopherOrchLib.gopher_orch_last_error(); - if (errorPtr) { - // Decode the struct from the pointer - const errorInfo = koffi.decode(errorPtr, ErrorInfoType); - if (errorInfo && errorInfo.message) { - return errorInfo.message; - } - } - } catch (e: any) { - // Silently fail - will return null - } - } - return null; - }, - - gopher_orch_clear_error: () => { - if (gopherOrchLib.gopher_orch_clear_error) { - gopherOrchLib.gopher_orch_clear_error(); - } - }, - - gopher_orch_free: (ptr: any) => { - if (gopherOrchLib.gopher_orch_free && ptr) { - gopherOrchLib.gopher_orch_free(ptr); - } - }, - - gopher_orch_agent_create_by_json: (provider: string, model: string, serverJson: string) => { - if (gopherOrchLib.gopher_orch_agent_create_by_json) { - // Configure environment for HTTP/HTTPS handling - const originalEnv = { - CURL_CA_BUNDLE: process.env.CURL_CA_BUNDLE, - SSL_VERIFY_PEER: process.env.SSL_VERIFY_PEER, - SSL_VERIFY_HOST: process.env.SSL_VERIFY_HOST - }; - - // For HTTP URLs, disable SSL verification - process.env.SSL_VERIFY_PEER = "0"; - process.env.SSL_VERIFY_HOST = "0"; - process.env.CURL_CA_BUNDLE = ""; - - try { - const handle = gopherOrchLib.gopher_orch_agent_create_by_json(provider, model, serverJson); - return handle ? { handle, isNull: () => !handle } : null; - } catch (ffiError: any) { - console.error('FFI Error in agent creation:', ffiError.message); - return null; - } finally { - // Restore original environment - for (const [key, value] of Object.entries(originalEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } - } - // FFI function not available, use fallback - console.warn('FFI function gopher_orch_agent_create_by_json not available, using fallback'); - return { handle: 'mock-agent-json', isNull: () => false }; - }, - - gopher_orch_agent_create_by_api_key: (provider: string, model: string, apiKey: string) => { - if (gopherOrchLib.gopher_orch_agent_create_by_api_key) { - // Configure environment for HTTP/HTTPS handling (same as JSON version) - const originalEnv = { - CURL_CA_BUNDLE: process.env.CURL_CA_BUNDLE, - SSL_VERIFY_PEER: process.env.SSL_VERIFY_PEER, - SSL_VERIFY_HOST: process.env.SSL_VERIFY_HOST - }; - - // For HTTP URLs, disable SSL verification - process.env.SSL_VERIFY_PEER = "0"; - process.env.SSL_VERIFY_HOST = "0"; - process.env.CURL_CA_BUNDLE = ""; - - try { - const handle = gopherOrchLib.gopher_orch_agent_create_by_api_key(provider, model, apiKey); - return handle ? { handle, isNull: () => !handle } : null; - } finally { - // Restore original environment - for (const [key, value] of Object.entries(originalEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } - } - // Fallback - return { handle: 'mock-agent-api', isNull: () => false }; - }, - - gopher_orch_agent_release: (agent: any) => { - if (gopherOrchLib.gopher_orch_agent_release && agent?.handle) { - gopherOrchLib.gopher_orch_agent_release(agent.handle); - } - }, - - gopher_orch_agent_run: (agent: any, query: string, timeoutMs: number = 30000) => { - if (gopherOrchLib.gopher_orch_agent_run && agent?.handle) { - const result = gopherOrchLib.gopher_orch_agent_run(agent.handle, query, timeoutMs); - return result || `No response for query: "${query}"`; - } - // Fallback - return `[FFI Fallback] Agent processed query: "${query}" - Real gopher-orch library not available`; - }, - - gopher_orch_api_fetch_servers: (apiKey: string) => { - if (gopherOrchLib.gopher_orch_api_fetch_servers) { - return gopherOrchLib.gopher_orch_api_fetch_servers(apiKey); - } - // Fallback - return JSON.stringify({ - succeeded: true, - code: 200000000, - message: "success - fallback", - data: { - servers: [ - { - version: "2025-01-09", - serverId: "1877234567890123456", - name: "fallback-server", - transport: "http_sse", - config: { - url: "http://127.0.0.1:3001/rpc", - headers: {} - }, - connectTimeout: 5000, - requestTimeout: 30000 - } - ] - } - }); - } -}; - -// Helper functions -export function initializeLibrary(): boolean { - return true; -} - -export function shutdownLibrary(): void { - // Silent shutdown -} - -export function getLastError(): string | null { - return null; -} - -export function clearError(): void { - // No-op -} \ No newline at end of file diff --git a/third_party/gopher-orch/sdk/typescript/src/ffi_bridge_old.js b/third_party/gopher-orch/sdk/typescript/src/ffi_bridge_old.js deleted file mode 100644 index a638405f..00000000 --- a/third_party/gopher-orch/sdk/typescript/src/ffi_bridge_old.js +++ /dev/null @@ -1,225 +0,0 @@ -/** - * @file ffi_bridge.js - * @brief Native Node.js addon bridge to gopher-orch C++ library - * - * This creates a simple bridge using Node.js addon capabilities - * to call the real C++ FFI functions directly. - */ - -const path = require('path'); -const fs = require('fs'); - -// Find the gopher-orch shared library -function findGopherOrchLibrary() { - const possiblePaths = [ - '../lib/libgopher-orch.dylib', // From SDK dist, look in examples lib - '../../examples/sdk/typescript/lib/libgopher-orch.dylib', // From SDK root - '../../../build/lib/libgopher-orch.dylib', // From examples, look in build - './lib/libgopher-orch.dylib' // Local lib directory - ]; - - for (const libPath of possiblePaths) { - const fullPath = path.resolve(__dirname, libPath); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - - throw new Error('Could not find libgopher-orch library. Build the project first.'); -} - -// Simple FFI bridge using Node.js addon pattern -class FFIBridge { - constructor() { - this.libPath = findGopherOrchLibrary(); - this.nativeAddon = null; - this.loadNativeAddon(); - } - - loadNativeAddon() { - try { - console.log(`🔗 Loading FFI bridge from: ${this.libPath}`); - - // Try to use the real FFI executor first (calls actual C++ binary) - try { - const { getRealFFIExecutor } = require('./real_ffi_executor.js'); - const realFFI = getRealFFIExecutor(); - - // Wrap real FFI calls with proper error handling - this.nativeAddon = { - gopher_orch_init: () => realFFI.init(), - gopher_orch_shutdown: () => realFFI.shutdown(), - gopher_orch_agent_create_by_json: (provider, model, serverJson) => { - try { - const handle = nativeFFI.createAgentByJson(provider, model, serverJson); - // Wrap the native handle to provide isNull() method - return { - nativeHandle: handle, - provider: provider, - model: model, - isNull: () => handle === null || handle === undefined - }; - } catch (error) { - console.error('Failed to create agent by JSON:', error.message); - return { nativeHandle: null, isNull: () => true }; - } - }, - gopher_orch_agent_create_by_api_key: (provider, model, apiKey) => { - try { - const handle = nativeFFI.createAgentByApiKey(provider, model, apiKey); - return { - nativeHandle: handle, - provider: provider, - model: model, - apiKey: apiKey, - isNull: () => handle === null || handle === undefined - }; - } catch (error) { - console.error('Failed to create agent by API key:', error.message); - return { nativeHandle: null, isNull: () => true }; - } - }, - gopher_orch_agent_run: (agent, query, timeoutMs) => { - try { - if (!agent || !agent.nativeHandle) { - throw new Error('Invalid agent handle'); - } - return nativeFFI.runAgent(agent.nativeHandle, query, timeoutMs); - } catch (error) { - console.error('Failed to run agent:', error.message); - return `❌ Agent execution failed: ${error.message}\n\nQuery: "${query}"\nThis indicates an issue with the native FFI bridge or C++ library.`; - } - }, - gopher_orch_agent_release: (agent) => { - try { - if (agent && agent.nativeHandle) { - nativeFFI.releaseAgent(agent.nativeHandle); - console.log(`🗑️ Released native agent handle`); - } - } catch (error) { - console.error('Failed to release agent:', error.message); - } - }, - gopher_orch_api_fetch_servers: (apiKey) => { - try { - return nativeFFI.fetchServers(apiKey); - } catch (error) { - console.error('Failed to fetch servers:', error.message); - // Return fallback configuration - return JSON.stringify({ - succeeded: false, - message: `API fetch failed: ${error.message}` - }); - } - } - }; - - console.log('✅ Native FFI bridge loaded successfully'); - return; - - } catch (nativeError) { - console.warn('⚠️ Native FFI failed, using enhanced mock:', nativeError.message); - } - - // Fallback to enhanced mock implementation - this.nativeAddon = { - gopher_orch_init: () => 0, - gopher_orch_shutdown: () => {}, - gopher_orch_agent_create_by_json: (provider, model, serverJson) => { - return { - id: `enhanced_mock_${Date.now()}`, - provider: provider, - model: model, - isNull: () => false - }; - }, - gopher_orch_agent_create_by_api_key: (provider, model, apiKey) => { - return { - id: `enhanced_mock_api_${Date.now()}`, - provider: provider, - model: model, - apiKey: apiKey, - isNull: () => false - }; - }, - gopher_orch_agent_run: (agent, query, timeoutMs) => { - const duration = Math.floor(Math.random() * 2000) + 500; - return `🔄 Enhanced Mock Response (Native FFI unavailable) - -Query: "${query}" -Agent: ${agent.provider} ${agent.model} -Processing Time: ${duration}ms -Library: ${this.libPath} - -📝 This is an enhanced mock response because the native FFI bridge could not load. - To get real AI responses: - 1. Ensure the C++ library is properly compiled with FFI exports - 2. Set a valid ANTHROPIC_API_KEY - 3. Verify MCP servers are accessible - -Fallback Status: Enhanced mock operational, native FFI not available.`; - }, - gopher_orch_agent_release: (agent) => { - console.log(`🗑️ Released mock agent: ${agent.id}`); - }, - gopher_orch_api_fetch_servers: (apiKey) => { - return JSON.stringify({ - succeeded: true, - code: 200000000, - message: "success (mock)", - data: { - servers: [ - { - version: "2025-01-09", - serverId: "1877234567890123456", - name: "enhanced-mock-server", - transport: "http_sse", - config: { - url: "http://127.0.0.1:3001/rpc", - headers: {} - }, - connectTimeout: 5000, - requestTimeout: 30000 - } - ] - } - }); - } - }; - - console.log('✅ Enhanced mock FFI bridge loaded'); - - } catch (error) { - console.error('❌ Failed to load FFI bridge:', error.message); - this.nativeAddon = null; - } - } - - call(funcName, ...args) { - if (this.nativeAddon && this.nativeAddon[funcName]) { - try { - console.log(`📞 Bridge calling: ${funcName}`); - const result = this.nativeAddon[funcName](...args); - console.log(`✅ Bridge call completed: ${funcName}`); - return result; - } catch (error) { - console.error(`❌ Bridge call failed ${funcName}:`, error.message); - throw error; - } - } else { - throw new Error(`Function ${funcName} not available in FFI bridge`); - } - } -} - -// Export singleton instance -let bridgeInstance = null; - -function getBridge() { - if (!bridgeInstance) { - bridgeInstance = new FFIBridge(); - } - return bridgeInstance; -} - -module.exports = { getBridge }; \ No newline at end of file diff --git a/third_party/gopher-orch/sdk/typescript/src/index.ts b/third_party/gopher-orch/sdk/typescript/src/index.ts deleted file mode 100644 index 7d0a5607..00000000 --- a/third_party/gopher-orch/sdk/typescript/src/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file index.ts - * @brief Main entry point for gopher-orch TypeScript SDK - * - * @example - * ```typescript - * import { GopherAgent } from "gopher-orch-sdk"; - * - * // Initialize (optional - happens automatically on first create) - * GopherAgent.init(); - * - * // Create an agent - * const agent = GopherAgent.create({ - * provider: 'AnthropicProvider', - * model: 'claude-3-haiku-20240307', - * apiKey: 'your-api-key' - * }); - * - * // Run queries - * const answer = agent.run("What time is it?"); - * console.log(answer); - * ``` - */ - -// Main exports -export { GopherAgent, GopherAgentConfig, ServerConfig } from './agent.js'; - -// Backward compatibility exports -export { ReActAgent, ServerConfigHelper } from './agent.js'; - -// Type exports -export * from './types.js'; - -// Low-level exports (for advanced usage) -export { library, initializeLibrary, shutdownLibrary } from './ffi.js'; - -/** - * Library version information - */ -export const version = '1.0.0'; diff --git a/third_party/gopher-orch/sdk/typescript/src/native_ffi.js b/third_party/gopher-orch/sdk/typescript/src/native_ffi.js deleted file mode 100644 index 502515b6..00000000 --- a/third_party/gopher-orch/sdk/typescript/src/native_ffi.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file native_ffi.js - * @brief Native FFI implementation using Node.js process.dlopen - * - * This loads the actual compiled C++ gopher-orch library and calls - * the real FFI functions directly, just like the C++ examples do. - */ - -const path = require('path'); -const fs = require('fs'); -const os = require('os'); - -// Find the gopher-orch shared library -function findGopherOrchLibrary() { - const possiblePaths = [ - '../lib/libgopher-orch.dylib', // From SDK dist, look in examples lib - '../../examples/sdk/typescript/lib/libgopher-orch.dylib', // From SDK root - '../../../build/lib/libgopher-orch.dylib', // From examples, look in build - './lib/libgopher-orch.dylib' // Local lib directory - ]; - - for (const libPath of possiblePaths) { - const fullPath = path.resolve(__dirname, libPath); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - - throw new Error('Could not find libgopher-orch library. Build the project first.'); -} - -// Native FFI implementation using process.dlopen -class NativeFFI { - constructor() { - this.libPath = findGopherOrchLibrary(); - this.lib = null; - this.loadLibrary(); - } - - loadLibrary() { - try { - console.log(`🔗 Loading native library: ${this.libPath}`); - - // Check if process.dlopen is available - if (typeof process.dlopen !== 'function') { - throw new Error('process.dlopen not available in this Node.js version'); - } - - // Use Node.js process.dlopen to load the shared library - this.lib = {}; - - // Load the library with default flags - try { - process.dlopen(this.lib, this.libPath); - console.log('✅ process.dlopen successful'); - } catch (dlopenError) { - console.error('❌ process.dlopen failed:', dlopenError.message); - console.log('🔄 Attempting alternative loading method...'); - - // Alternative: try using require() if the library has a Node.js binding - try { - this.lib = require(this.libPath); - console.log('✅ Alternative require() loading successful'); - } catch (requireError) { - console.error('❌ Alternative loading also failed:', requireError.message); - throw new Error(`Both dlopen and require failed: ${dlopenError.message}`); - } - } - - // Verify the library was loaded - if (!this.lib || typeof this.lib !== 'object') { - throw new Error('Library loaded but no exports available'); - } - - console.log('✅ Native library loaded successfully'); - - // Debug: show what was actually loaded - const allKeys = Object.keys(this.lib); - console.log(`📋 Library exports (${allKeys.length} total):`); - console.log(` First 10: ${allKeys.slice(0, 10).join(', ')}${allKeys.length > 10 ? '...' : ''}`); - - // Verify that expected FFI functions are available - const expectedFunctions = [ - 'gopher_orch_init', - 'gopher_orch_shutdown', - 'gopher_orch_agent_create_by_json', - 'gopher_orch_agent_create_by_api_key', - 'gopher_orch_agent_run', - 'gopher_orch_agent_release', - 'gopher_orch_api_fetch_servers' - ]; - - const availableFunctions = []; - for (const funcName of expectedFunctions) { - if (this.lib[funcName] && typeof this.lib[funcName] === 'function') { - availableFunctions.push(funcName); - } - } - - console.log(`📋 Available FFI functions: ${availableFunctions.length}/${expectedFunctions.length}`); - if (availableFunctions.length > 0) { - console.log(` ✓ ${availableFunctions.join(', ')}`); - } - - if (availableFunctions.length === 0) { - console.warn('⚠️ No expected FFI functions found in library'); - console.log(' This likely means the library was built without FFI exports.'); - console.log(' Available functions:', allKeys.filter(k => typeof this.lib[k] === 'function').slice(0, 5).join(', ') + '...'); - } - - } catch (error) { - console.error('❌ Failed to load native library:', error.message); - this.lib = null; - } - } - - // Call a C++ FFI function with proper argument handling - callFunction(funcName, ...args) { - if (!this.lib) { - throw new Error('Native library not loaded'); - } - - if (!this.lib[funcName]) { - throw new Error(`Function ${funcName} not found in library`); - } - - try { - console.log(`🔧 Calling native function: ${funcName}(${args.map(a => typeof a).join(', ')})`); - - // Call the actual C++ FFI function - const result = this.lib[funcName](...args); - - console.log(`✅ Native function ${funcName} completed`); - return result; - - } catch (error) { - console.error(`❌ Error calling ${funcName}:`, error.message); - throw error; - } - } - - // Initialize the gopher-orch library - init() { - return this.callFunction('gopher_orch_init'); - } - - // Shutdown the gopher-orch library - shutdown() { - return this.callFunction('gopher_orch_shutdown'); - } - - // Create agent by JSON configuration - createAgentByJson(provider, model, serverJson) { - return this.callFunction('gopher_orch_agent_create_by_json', provider, model, serverJson); - } - - // Create agent by API key - createAgentByApiKey(provider, model, apiKey) { - return this.callFunction('gopher_orch_agent_create_by_api_key', provider, model, apiKey); - } - - // Run agent query - runAgent(agent, query, timeoutMs) { - return this.callFunction('gopher_orch_agent_run', agent, query, timeoutMs || 30000); - } - - // Release agent resources - releaseAgent(agent) { - return this.callFunction('gopher_orch_agent_release', agent); - } - - // Fetch MCP servers from API - fetchServers(apiKey) { - return this.callFunction('gopher_orch_api_fetch_servers', apiKey); - } -} - -// Export singleton instance -let nativeFFIInstance = null; - -function getNativeFFI() { - if (!nativeFFIInstance) { - nativeFFIInstance = new NativeFFI(); - } - return nativeFFIInstance; -} - -module.exports = { getNativeFFI }; \ No newline at end of file diff --git a/third_party/gopher-orch/sdk/typescript/src/real_ffi_executor.js b/third_party/gopher-orch/sdk/typescript/src/real_ffi_executor.js deleted file mode 100644 index c69327a6..00000000 --- a/third_party/gopher-orch/sdk/typescript/src/real_ffi_executor.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * @file real_ffi_executor.js - * @brief Real FFI executor using the working C++ binary - * - * This calls the actual compiled C++ client_example binary which has - * all the FFI functions working correctly and gets real AI responses. - */ - -const path = require('path'); -const fs = require('fs'); -const { execSync } = require('child_process'); - -// Find the C++ client example binary -function findClientExampleBinary() { - const possiblePaths = [ - '../../../build/bin/examples/sdk/client_example', - '../../../../build/bin/examples/sdk/client_example', - '../../../bin/client_example', - '../../../examples/sdk/client_example' - ]; - - for (const binaryPath of possiblePaths) { - const fullPath = path.resolve(__dirname, binaryPath); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - - throw new Error('Could not find client_example binary. Build the project first.'); -} - -// Real FFI executor using the C++ binary -class RealFFIExecutor { - constructor() { - this.binaryPath = findClientExampleBinary(); - console.log(`🎯 Using real C++ binary: ${this.binaryPath}`); - } - - // Initialize - just verify binary exists - init() { - if (!fs.existsSync(this.binaryPath)) { - throw new Error('C++ binary not available'); - } - return 0; - } - - // Shutdown - no-op for binary - shutdown() { - // Binary handles its own cleanup - } - - // Create agent by calling the binary with configuration - createAgentByJson(provider, model, serverJson) { - // For this approach, we just store the configuration - // and use it when running queries - return { - type: 'json', - provider, - model, - serverJson, - isNull: () => false - }; - } - - // Create agent by API key - createAgentByApiKey(provider, model, apiKey) { - return { - type: 'api_key', - provider, - model, - apiKey, - isNull: () => false - }; - } - - // Run agent query - this calls the real C++ binary - runAgent(agent, query, timeoutMs = 30000) { - console.log(`🚀 Executing real C++ agent for query: "${query}"`); - - try { - // Set up environment for the C++ binary - const env = { ...process.env }; - - // Use the agent's API key if available - if (agent.apiKey) { - env.ANTHROPIC_API_KEY = agent.apiKey; - } - - // Ensure we have an API key - if (!env.ANTHROPIC_API_KEY) { - throw new Error('ANTHROPIC_API_KEY not set'); - } - - // Set library paths - const libDir = path.resolve(__dirname, '../../../examples/sdk/typescript/lib'); - if (fs.existsSync(libDir)) { - env.DYLD_LIBRARY_PATH = `${libDir}:${env.DYLD_LIBRARY_PATH || ''}`; - env.LD_LIBRARY_PATH = `${libDir}:${env.LD_LIBRARY_PATH || ''}`; - } - - // Execute the real C++ binary with the query - const startTime = Date.now(); - const result = execSync(`"${this.binaryPath}" "${query}"`, { - env, - encoding: 'utf8', - timeout: timeoutMs, - maxBuffer: 1024 * 1024 // 1MB buffer - }); - - const duration = Date.now() - startTime; - console.log(`✅ Real C++ agent completed in ${duration}ms`); - - // Parse the output to extract the actual agent response - const lines = result.split('\n'); - const responseStart = lines.findIndex(line => line.includes('Agent Response:') || line.includes('=== Agent Response ===')); - const responseEnd = lines.findIndex((line, index) => index > responseStart && (line.includes('===') || line.includes('Status:') || line.includes('Total execution time:'))); - - if (responseStart >= 0) { - let response = lines.slice(responseStart + 1, responseEnd >= 0 ? responseEnd : undefined).join('\n').trim(); - - // Clean up the response to extract just the AI answer - response = response.replace(/^-+$|^=+$/gm, '').trim(); - - if (response) { - return `🤖 Real AI Response (via C++ FFI):\n\n${response}\n\n⚡ Powered by: ${agent.provider} ${agent.model}\n🔗 Execution: C++ gopher-orch library\n⏱️ Duration: ${duration}ms`; - } - } - - // If we can't parse a specific response, return the relevant output - const relevantOutput = result.split('\n') - .filter(line => !line.includes('Loading MCP server') && - !line.includes('Configuration loaded') && - !line.includes('Available tools:') && - !line.includes('Creating agent') && - line.trim().length > 0) - .join('\n') - .trim(); - - return `🤖 Real AI Response (via C++ FFI):\n\n${relevantOutput}\n\n⚡ Powered by: ${agent.provider} ${agent.model}\n🔗 Execution: C++ gopher-orch library\n⏱️ Duration: ${duration}ms`; - - } catch (error) { - if (error.status === 124 || error.signal === 'SIGTERM') { - throw new Error(`Query execution timed out after ${timeoutMs}ms`); - } else if (error.stdout && error.stdout.includes('Agent error:')) { - // Extract the actual error message - const errorMatch = error.stdout.match(/Agent error: (.+)/); - const errorMsg = errorMatch ? errorMatch[1] : error.message; - throw new Error(`Real agent error: ${errorMsg}`); - } - throw new Error(`Real agent execution failed: ${error.message}`); - } - } - - // Release agent - no-op for binary approach - releaseAgent(agent) { - console.log(`🗑️ Released agent: ${agent.type}_${agent.provider}_${agent.model}`); - } - - // Fetch servers using the binary (if available) - fetchServers(apiKey) { - // For now, return a basic configuration - // In the future, could call a specific API fetcher binary - return JSON.stringify({ - succeeded: true, - code: 200000000, - message: "success", - data: { - servers: [ - { - version: "2025-01-09", - serverId: "1877234567890123456", - name: "real-binary-server", - transport: "http_sse", - config: { - url: "http://127.0.0.1:3001/rpc", - headers: {} - }, - connectTimeout: 5000, - requestTimeout: 30000 - } - ] - } - }); - } -} - -// Export singleton instance -let executorInstance = null; - -function getRealFFIExecutor() { - if (!executorInstance) { - executorInstance = new RealFFIExecutor(); - } - return executorInstance; -} - -module.exports = { getRealFFIExecutor }; \ No newline at end of file diff --git a/third_party/gopher-orch/sdk/typescript/src/types.ts b/third_party/gopher-orch/sdk/typescript/src/types.ts deleted file mode 100644 index 6def22e3..00000000 --- a/third_party/gopher-orch/sdk/typescript/src/types.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @file types.ts - * @brief TypeScript type definitions for gopher-orch SDK - */ - -export interface ServerConfig { - version: string; - serverId: string; - name: string; - transport: string; - config: { - url: string; - headers: Record; - }; - connectTimeout: number; - requestTimeout: number; -} - -export interface ApiResponse { - succeeded: boolean; - code: number; - message: string; - data: { - servers: ServerConfig[]; - }; -} - -export interface AgentConfig { - provider: string; - model: string; - systemPrompt?: string; - maxIterations?: number; - temperature?: number; -} - -export interface AgentResult { - response: string; - status: 'success' | 'error' | 'timeout'; - iterationCount?: number; - tokensUsed?: number; -} - -export class AgentError extends Error { - constructor(message: string, public code?: string) { - super(message); - this.name = 'AgentError'; - } -} - -export class ApiKeyError extends AgentError { - constructor(message: string = 'Invalid or missing API key') { - super(message, 'API_KEY_ERROR'); - } -} - -export class ConnectionError extends AgentError { - constructor(message: string = 'Failed to connect to MCP servers') { - super(message, 'CONNECTION_ERROR'); - } -} - -export class TimeoutError extends AgentError { - constructor(message: string = 'Agent execution timed out') { - super(message, 'TIMEOUT_ERROR'); - } -} \ No newline at end of file diff --git a/third_party/gopher-orch/sdk/typescript/tsconfig.json b/third_party/gopher-orch/sdk/typescript/tsconfig.json deleted file mode 100644 index adef7b62..00000000 --- a/third_party/gopher-orch/sdk/typescript/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] -} \ No newline at end of file diff --git a/third_party/gopher-orch/src/CMakeLists.txt b/third_party/gopher-orch/src/CMakeLists.txt deleted file mode 100644 index c74ef443..00000000 --- a/third_party/gopher-orch/src/CMakeLists.txt +++ /dev/null @@ -1,255 +0,0 @@ -# gopher-orch source files - -# Core library sources (orch-specific extensions) -set(ORCH_CORE_SOURCES - orch/hello.cc -) - -# MCP Server sources (requires gopher-mcp) -# Only include when gopher-mcp is available -set(ORCH_MCP_SOURCES "") -if(NOT BUILD_WITHOUT_GOPHER_MCP) - set(ORCH_MCP_SOURCES - gopher/orch/server/mcp_server.cc - gopher/orch/server/rest_server.cc - gopher/orch/server/curl_http_client.cc - gopher/orch/server/gateway_server.cpp - ) -endif() - -# LLM Provider sources (requires gopher-mcp for HTTP client) -set(ORCH_LLM_SOURCES "") -if(NOT BUILD_WITHOUT_GOPHER_MCP) - set(ORCH_LLM_SOURCES - gopher/orch/llm/openai_provider.cc - gopher/orch/llm/anthropic_provider.cc - gopher/orch/llm/llm_factory.cc - gopher/orch/llm/llm_runnable.cc - ) -endif() - -# Agent sources (requires LLM providers) -set(ORCH_AGENT_SOURCES "") -if(NOT BUILD_WITHOUT_GOPHER_MCP) - set(ORCH_AGENT_SOURCES - gopher/orch/agent/agent.cc - gopher/orch/agent/api_engine.cc - gopher/orch/agent/config_loader.cc - gopher/orch/agent/tool_registry.cc - gopher/orch/agent/tool_runnable.cc - gopher/orch/agent/agent_runnable.cc - gopher/orch/agent/tools_fetcher.cpp - ) -endif() - -# FFI sources (provides C API for agents) -set(ORCH_FFI_SOURCES "") -if(NOT BUILD_WITHOUT_GOPHER_MCP) - set(ORCH_FFI_SOURCES - gopher/orch/ffi/orch_ffi_agent.cc - ) -endif() - -# Combine all sources -set(GOPHER_ORCH_SOURCES - ${ORCH_CORE_SOURCES} - ${ORCH_MCP_SOURCES} - ${ORCH_LLM_SOURCES} - ${ORCH_AGENT_SOURCES} - ${ORCH_FFI_SOURCES} -) - -# Build static library -if(BUILD_STATIC_LIBS) - add_library(gopher-orch-static STATIC ${GOPHER_ORCH_SOURCES}) - target_include_directories(gopher-orch-static PUBLIC - $ - $ - $ - $ - ) - - # Add include directories for gopher-mcp's dependencies (fmt, nlohmann_json, etc.) - # These are needed when including gopher-mcp headers that use these libraries - if(NOT BUILD_WITHOUT_GOPHER_MCP) - # These paths are set by gopher-mcp's FetchContent - if(TARGET fmt) - get_target_property(FMT_INCLUDE_DIR fmt INTERFACE_INCLUDE_DIRECTORIES) - if(FMT_INCLUDE_DIR) - target_include_directories(gopher-orch-static PUBLIC ${FMT_INCLUDE_DIR}) - endif() - endif() - if(TARGET nlohmann_json) - get_target_property(NLOHMANN_JSON_INCLUDE_DIR nlohmann_json INTERFACE_INCLUDE_DIRECTORIES) - if(NLOHMANN_JSON_INCLUDE_DIR) - target_include_directories(gopher-orch-static PUBLIC ${NLOHMANN_JSON_INCLUDE_DIR}) - endif() - endif() - endif() - - # Link dependencies - if(NOT BUILD_WITHOUT_GOPHER_MCP) - target_link_libraries(gopher-orch-static PUBLIC - ${GOPHER_MCP_LIBRARIES} - ${CURL_LIBRARIES} - Threads::Threads - ) - # Define GOPHER_ORCH_WITH_MCP to enable MCP-specific code - # MCP_USE_STD_OPTIONAL_VARIANT=0 ensures ABI compatibility with gopher-mcp library - # (gopher-mcp uses mcp::optional/variant, not std:: types) - target_compile_definitions(gopher-orch-static PUBLIC - GOPHER_ORCH_WITH_MCP - MCP_USE_STD_OPTIONAL_VARIANT=0 - ) - else() - target_link_libraries(gopher-orch-static PUBLIC - Threads::Threads - ) - endif() - - set_target_properties(gopher-orch-static PROPERTIES - OUTPUT_NAME gopher-orch - POSITION_INDEPENDENT_CODE ON - ) - - # Set the main library alias - add_library(gopher-orch ALIAS gopher-orch-static) - - # Installation - install(TARGETS gopher-orch-static - EXPORT gopher-orch-targets - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib - RUNTIME DESTINATION bin - COMPONENT libraries - ) -endif() - -# Build shared library -if(BUILD_SHARED_LIBS) - add_library(gopher-orch-shared SHARED ${GOPHER_ORCH_SOURCES}) - target_include_directories(gopher-orch-shared PUBLIC - $ - $ - $ - $ - ) - - # Add include directories for gopher-mcp's dependencies (fmt, nlohmann_json, etc.) - if(NOT BUILD_WITHOUT_GOPHER_MCP) - if(TARGET fmt) - get_target_property(FMT_INCLUDE_DIR fmt INTERFACE_INCLUDE_DIRECTORIES) - if(FMT_INCLUDE_DIR) - target_include_directories(gopher-orch-shared PUBLIC ${FMT_INCLUDE_DIR}) - endif() - endif() - if(TARGET nlohmann_json) - get_target_property(NLOHMANN_JSON_INCLUDE_DIR nlohmann_json INTERFACE_INCLUDE_DIRECTORIES) - if(NLOHMANN_JSON_INCLUDE_DIR) - target_include_directories(gopher-orch-shared PUBLIC ${NLOHMANN_JSON_INCLUDE_DIR}) - endif() - endif() - endif() - - # Link dependencies - if(NOT BUILD_WITHOUT_GOPHER_MCP) - target_link_libraries(gopher-orch-shared PUBLIC - ${GOPHER_MCP_LIBRARIES} - ${CURL_LIBRARIES} - Threads::Threads - ) - # Define GOPHER_ORCH_WITH_MCP to enable MCP-specific code - # MCP_USE_STD_OPTIONAL_VARIANT=0 ensures ABI compatibility with gopher-mcp library - # (gopher-mcp uses mcp::optional/variant, not std:: types) - target_compile_definitions(gopher-orch-shared PUBLIC - GOPHER_ORCH_WITH_MCP - MCP_USE_STD_OPTIONAL_VARIANT=0 - ) - else() - target_link_libraries(gopher-orch-shared PUBLIC - Threads::Threads - ) - endif() - - set_target_properties(gopher-orch-shared PROPERTIES - OUTPUT_NAME gopher-orch - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} - ) - - # If only building shared, set it as the main library - if(NOT BUILD_STATIC_LIBS) - add_library(gopher-orch ALIAS gopher-orch-shared) - endif() - - # Installation - install(TARGETS gopher-orch-shared - EXPORT gopher-orch-targets - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib - RUNTIME DESTINATION bin - COMPONENT libraries - ) -endif() - -# Export targets only when not using submodule -# (When using submodule, gopher-mcp targets aren't installable) -if(NOT USE_SUBMODULE_GOPHER_MCP) - install(EXPORT gopher-orch-targets - FILE gopher-orch-targets.cmake - NAMESPACE gopher-orch:: - DESTINATION lib/cmake/gopher-orch - COMPONENT development - ) -endif() - -# ═══════════════════════════════════════════════════════════════════════════════ -# MCP Gateway Production Binary -# ═══════════════════════════════════════════════════════════════════════════════ -if(NOT BUILD_WITHOUT_GOPHER_MCP) - add_executable(mcp_gateway - gopher/orch/server/mcp_gateway_main.cpp - ) - - target_include_directories(mcp_gateway PRIVATE - ${CMAKE_SOURCE_DIR}/include - ${GOPHER_MCP_INCLUDE_DIR} - ${CURL_INCLUDE_DIRS} - ) - - # Add include directories for gopher-mcp's dependencies - if(TARGET fmt) - get_target_property(FMT_INCLUDE_DIR fmt INTERFACE_INCLUDE_DIRECTORIES) - if(FMT_INCLUDE_DIR) - target_include_directories(mcp_gateway PRIVATE ${FMT_INCLUDE_DIR}) - endif() - endif() - if(TARGET nlohmann_json) - get_target_property(NLOHMANN_JSON_INCLUDE_DIR nlohmann_json INTERFACE_INCLUDE_DIRECTORIES) - if(NLOHMANN_JSON_INCLUDE_DIR) - target_include_directories(mcp_gateway PRIVATE ${NLOHMANN_JSON_INCLUDE_DIR}) - endif() - endif() - - target_link_libraries(mcp_gateway - gopher-orch - ${GOPHER_MCP_LIBRARIES} - ${CURL_LIBRARIES} - Threads::Threads - ) - - target_compile_definitions(mcp_gateway PRIVATE - GOPHER_ORCH_WITH_MCP - MCP_USE_STD_OPTIONAL_VARIANT=0 - ) - - set_target_properties(mcp_gateway PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - ) - - # Installation - install(TARGETS mcp_gateway - RUNTIME DESTINATION bin - COMPONENT runtime - ) -endif() diff --git a/third_party/gopher-orch/src/gopher/orch/agent/agent.cc b/third_party/gopher-orch/src/gopher/orch/agent/agent.cc deleted file mode 100644 index 40ecc49c..00000000 --- a/third_party/gopher-orch/src/gopher/orch/agent/agent.cc +++ /dev/null @@ -1,751 +0,0 @@ -// ReActAgent Implementation - -#include "gopher/orch/agent/agent.h" - -#include -#include -#include -#include -#include -#include -#include "mcp/event/libevent_dispatcher.h" -#include "gopher/orch/agent/tools_fetcher.h" -#include "gopher/orch/agent/api_engine.h" -#include "gopher/orch/llm/anthropic_provider.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; - -// ═══════════════════════════════════════════════════════════════════════════ -// IMPLEMENTATION -// ═══════════════════════════════════════════════════════════════════════════ - -class ReActAgent::Impl { - public: - LLMProviderPtr provider; - ToolRegistryPtr tools; - ToolExecutorPtr executor; - AgentConfig config; - AgentState state; - - // Callbacks - AgentCallback completion_callback; - StepCallback step_callback; - ToolApprovalCallback approval_callback; - - // Current dispatcher (set during run) - Dispatcher* dispatcher = nullptr; - - // Cancellation flag - std::atomic cancelled{false}; - - // Thread safety - mutable std::mutex mutex; - - Impl(LLMProviderPtr p, ToolRegistryPtr t, const AgentConfig& c) - : provider(std::move(p)), - tools(t ? t : makeToolRegistry()), - executor(makeToolExecutor(tools)), - config(c) {} - - // Build messages for LLM call - std::vector buildMessages() const { - std::vector messages; - - // Add system prompt if configured - if (!config.system_prompt.empty()) { - messages.push_back(Message::system(config.system_prompt)); - } - - // Add conversation history - for (const auto& msg : state.messages) { - messages.push_back(msg); - } - - return messages; - } - - // Get tool specs for LLM - std::vector getToolSpecs() const { - if (tools) { - return tools->getToolSpecs(); - } - return {}; - } - - // Record a step - void recordStep(const AgentStep& step) { - state.steps.push_back(step); - - // Update total usage - if (step.llm_usage.has_value()) { - state.total_usage.prompt_tokens += step.llm_usage->prompt_tokens; - state.total_usage.completion_tokens += step.llm_usage->completion_tokens; - state.total_usage.total_tokens += step.llm_usage->total_tokens; - } - - // Invoke step callback - if (step_callback) { - step_callback(step); - } - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// FACTORY METHODS -// ═══════════════════════════════════════════════════════════════════════════ - -ReActAgent::Ptr ReActAgent::create(LLMProviderPtr provider, - ToolRegistryPtr tools, - const AgentConfig& config) { - return Ptr(new ReActAgent(std::move(provider), std::move(tools), config)); -} - -ReActAgent::Ptr ReActAgent::create(LLMProviderPtr provider, - const AgentConfig& config) { - return create(std::move(provider), nullptr, config); -} - -ReActAgent::ReActAgent(LLMProviderPtr provider, - ToolRegistryPtr tools, - const AgentConfig& config) - : impl_(std::make_unique( - std::move(provider), std::move(tools), config)) {} - -ReActAgent::~ReActAgent() { - cancel(); - shutdownConnections(); -} - -void ReActAgent::shutdownConnections() { - // Shutdown MCP server connections if we own them - if (tools_fetcher_ && owned_dispatcher_) { - bool shutdown_complete = false; - tools_fetcher_->shutdown(*owned_dispatcher_, [&shutdown_complete]() { - shutdown_complete = true; - }); - - // Wait for shutdown to complete (with timeout to avoid hanging) - auto shutdown_start = std::chrono::steady_clock::now(); - auto shutdown_timeout = std::chrono::seconds(5); - - while (!shutdown_complete) { - owned_dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (std::chrono::steady_clock::now() - shutdown_start > shutdown_timeout) { - break; // Don't hang forever in destructor - } - } - } - - // Release resources - tools_fetcher_.reset(); - owned_dispatcher_.reset(); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// RUN METHODS -// ═══════════════════════════════════════════════════════════════════════════ - -void ReActAgent::run(const std::string& query, - Dispatcher& dispatcher, - AgentCallback callback) { - run(query, {}, dispatcher, std::move(callback)); -} - -void ReActAgent::run(const std::string& query, - const std::vector& context, - Dispatcher& dispatcher, - AgentCallback callback) { - // Check if already running - if (impl_->state.status == AgentStatus::RUNNING) { - dispatcher.post([callback = std::move(callback)]() { - callback(Result( - Error(AgentError::UNKNOWN, "Agent is already running"))); - }); - return; - } - - // Check provider - if (!impl_->provider) { - dispatcher.post([callback = std::move(callback)]() { - callback(Result( - Error(AgentError::NO_PROVIDER, "No LLM provider configured"))); - }); - return; - } - - // Initialize state - impl_->state = AgentState(); - impl_->state.status = AgentStatus::RUNNING; - impl_->state.start_time = std::chrono::steady_clock::now(); - impl_->cancelled = false; - - // Add context messages - for (const auto& msg : context) { - impl_->state.messages.push_back(msg); - } - - // Add user query - impl_->state.messages.push_back(Message::user(query)); - - // Store callback and dispatcher - impl_->completion_callback = std::move(callback); - impl_->dispatcher = &dispatcher; - - // Start the ReAct loop - executeLoop(dispatcher); -} - -void ReActAgent::cancel() { - impl_->cancelled = true; - - if (impl_->state.status == AgentStatus::RUNNING) { - impl_->state.status = AgentStatus::CANCELLED; - impl_->state.error = Error(AgentError::CANCELLED, "Agent cancelled"); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// STATE ACCESS -// ═══════════════════════════════════════════════════════════════════════════ - -const AgentState& ReActAgent::state() const { return impl_->state; } - -bool ReActAgent::isRunning() const { - return impl_->state.status == AgentStatus::RUNNING; -} - -void ReActAgent::setStepCallback(StepCallback callback) { - impl_->step_callback = std::move(callback); -} - -void ReActAgent::setToolApprovalCallback(ToolApprovalCallback callback) { - impl_->approval_callback = std::move(callback); -} - -LLMProviderPtr ReActAgent::provider() const { return impl_->provider; } - -ToolRegistryPtr ReActAgent::tools() const { return impl_->tools; } - -const AgentConfig& ReActAgent::config() const { return impl_->config; } - -void ReActAgent::setConfig(const AgentConfig& config) { - if (impl_->state.status != AgentStatus::RUNNING) { - impl_->config = config; - } -} - -void ReActAgent::addTool(const std::string& name, - const std::string& description, - const JsonValue& parameters, - ToolFunction function) { - if (impl_->tools) { - impl_->tools->addTool(name, description, parameters, std::move(function)); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// INTERNAL EXECUTION -// ═══════════════════════════════════════════════════════════════════════════ - -void ReActAgent::executeLoop(Dispatcher& dispatcher) { - // Check cancellation - if (impl_->cancelled) { - completeRun(AgentStatus::CANCELLED, dispatcher); - return; - } - - // Check iteration limit - if (impl_->state.current_iteration >= impl_->config.max_iterations) { - impl_->state.error = - Error(AgentError::MAX_ITERATIONS, "Maximum iterations reached"); - completeRun(AgentStatus::MAX_ITERATIONS_REACHED, dispatcher); - return; - } - - // Check timeout - auto elapsed = std::chrono::steady_clock::now() - impl_->state.start_time; - if (elapsed > impl_->config.timeout) { - impl_->state.error = Error(AgentError::TIMEOUT, "Agent timeout"); - completeRun(AgentStatus::FAILED, dispatcher); - return; - } - - impl_->state.current_iteration++; - - // Call LLM - callLLM(dispatcher); -} - -void ReActAgent::callLLM(Dispatcher& dispatcher) { - auto messages = impl_->buildMessages(); - auto tools = impl_->getToolSpecs(); - auto& config = impl_->config.llm_config; - - auto start_time = std::chrono::steady_clock::now(); - - impl_->provider->chat( - messages, tools, config, dispatcher, - [this, &dispatcher, start_time](Result result) { - if (!mcp::holds_alternative(result)) { - impl_->state.error = mcp::get(result); - completeRun(AgentStatus::FAILED, dispatcher); - return; - } - - auto duration = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time); - - const auto& response = mcp::get(result); - - // Create step record - AgentStep step; - step.step_number = impl_->state.current_iteration; - step.llm_message = response.message; - step.llm_usage = response.usage; - step.llm_duration = duration; - - // Record step first (will be updated with tool results if needed) - impl_->recordStep(step); - - // Handle response (may complete run or execute tools) - handleLLMResponse(response, dispatcher); - }); -} - -void ReActAgent::handleLLMResponse(const LLMResponse& response, - Dispatcher& dispatcher) { - // Add assistant message to history - impl_->state.messages.push_back(response.message); - - // Check if LLM wants to call tools - if (response.hasToolCalls()) { - // Execute tool calls - executeToolCalls(response.toolCalls(), dispatcher); - } else { - // No tool calls - agent is done - completeRun(AgentStatus::COMPLETED, dispatcher); - } -} - -void ReActAgent::executeToolCalls(const std::vector& calls, - Dispatcher& dispatcher) { - // Check for tool approval - if (impl_->approval_callback) { - for (const auto& call : calls) { - if (!impl_->approval_callback(call)) { - // Tool call rejected - impl_->state.error = - Error(AgentError::CANCELLED, "Tool call rejected: " + call.name); - completeRun(AgentStatus::CANCELLED, dispatcher); - return; - } - } - } - - if (!impl_->executor) { - // No executor configured - add error result - for (const auto& call : calls) { - impl_->state.messages.push_back( - Message::toolResult(call.id, "Error: No tools configured")); - } - // Continue loop - dispatcher.post([this, &dispatcher]() { executeLoop(dispatcher); }); - return; - } - - // Execute tools via executor - auto start_time = std::chrono::steady_clock::now(); - - impl_->executor->executeToolCalls( - calls, impl_->config.parallel_tool_calls, dispatcher, - [this, &dispatcher, calls, - start_time](std::vector> results) { - auto duration = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time); - - handleToolResults(calls, results, dispatcher); - }); -} - -void ReActAgent::handleToolResults( - const std::vector& calls, - const std::vector>& results, - Dispatcher& dispatcher) { - // Update last step with tool executions - if (!impl_->state.steps.empty()) { - auto& last_step = impl_->state.steps.back(); - for (size_t i = 0; i < calls.size(); ++i) { - ToolExecution exec; - exec.tool_name = calls[i].name; - exec.call_id = calls[i].id; - exec.input = calls[i].arguments; - - if (i < results.size()) { - if (mcp::holds_alternative(results[i])) { - exec.output = mcp::get(results[i]); - exec.success = true; - } else { - exec.success = false; - exec.error_message = mcp::get(results[i]).message; - } - } - - last_step.tool_executions.push_back(std::move(exec)); - } - } - - // Add tool results to messages - for (size_t i = 0; i < calls.size(); ++i) { - std::string result_content; - - if (i < results.size()) { - if (mcp::holds_alternative(results[i])) { - result_content = mcp::get(results[i]).toString(); - } else { - result_content = "Error: " + mcp::get(results[i]).message; - } - } else { - result_content = "Error: No result returned"; - } - - impl_->state.messages.push_back( - Message::toolResult(calls[i].id, result_content)); - } - - // Continue the loop - dispatcher.post([this, &dispatcher]() { executeLoop(dispatcher); }); -} - -void ReActAgent::completeRun(AgentStatus status, Dispatcher& dispatcher) { - impl_->state.status = status; - impl_->state.elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - impl_->state.start_time); - - auto result = buildResult(); - - if (impl_->completion_callback) { - auto callback = std::move(impl_->completion_callback); - impl_->completion_callback = nullptr; - - if (status == AgentStatus::COMPLETED) { - callback(Result(std::move(result))); - } else { - callback(Result(impl_->state.error.value_or( - Error(AgentError::UNKNOWN, "Unknown error")))); - } - } -} - -AgentResult ReActAgent::buildResult() const { - AgentResult result; - result.status = impl_->state.status; - result.messages = impl_->state.messages; - result.steps = impl_->state.steps; - result.total_usage = impl_->state.total_usage; - result.duration = impl_->state.elapsed; - result.error = impl_->state.error; - - // Get final response from last assistant message - for (auto it = impl_->state.messages.rbegin(); - it != impl_->state.messages.rend(); ++it) { - if (it->role == Role::ASSISTANT && !it->content.empty()) { - result.response = it->content; - break; - } - } - - return result; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// STATIC CONVENIENCE METHODS -// ═══════════════════════════════════════════════════════════════════════════ - -ReActAgent::Ptr ReActAgent::createByJson(const std::string& provider_name, - const std::string& model, - const std::string& server_json_config) { - using namespace gopher::orch::llm; - - // Validate API response format if it's an API response - try { - JsonValue json = JsonValue::parse(server_json_config); - - // Check if this is an API response format - if (json.contains("succeeded") && json.contains("message")) { - // Validate API response success - if (!json["succeeded"].getBool()) { - // API request failed - return nullptr (error will be handled by caller) - return nullptr; - } - - // Check message field for success status - std::string message = json["message"].getString(); - if (message != "success") { - // API response indicates failure - return nullptr - // The actual error message will be handled during tool loading - return nullptr; - } - } - } catch (const std::exception& e) { - // JSON parsing failed - return nullptr; - } - - // Create LLM provider - // Note: AnthropicProvider::create() will resolve API key from environment variable - // and throw std::runtime_error with helpful message if not set - LLMProviderPtr llm_provider; - if (provider_name == "AnthropicProvider" || provider_name == "anthropic") { - llm_provider = AnthropicProvider::create(""); - } else { - return nullptr; // Error: Unsupported provider - } - - if (!llm_provider) { - return nullptr; // Error: Failed to create provider - } - - // Load tools immediately from server config - auto dispatcher = std::make_unique("tools_loader"); - auto tools_fetcher = std::make_unique(); - - // Setup synchronization for async operations - bool load_complete = false; - bool load_success = false; - - // Load tools from config - tools_fetcher->loadFromJson(server_json_config, *dispatcher, - [&](VoidResult result) { - load_complete = true; - load_success = mcp::holds_alternative(result); - }); - - // Wait for loading to complete - auto load_start = std::chrono::steady_clock::now(); - auto load_timeout = std::chrono::seconds(15); - - while (!load_complete) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (std::chrono::steady_clock::now() - load_start > load_timeout) { - return nullptr; // Timeout loading tools - } - } - - if (!load_success) { - return nullptr; // Failed to load tools - } - - // Allow async operations to complete - for (int i = 0; i < 20; i++) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - - // Get the loaded tool registry - auto registry = tools_fetcher->getRegistry(); - if (!registry) { - return nullptr; // Failed to get registry - } - - // NOTE: Do NOT shutdown tools_fetcher here! - // The SSE connections must remain open for tool execution. - // The connections will be closed when the agent is destroyed. - - // Create agent with basic config - AgentConfig agent_config(model); - agent_config.withSystemPrompt( - "You are a helpful assistant with access to various tools. " - "Use the appropriate tools to complete tasks. " - "Always explain your reasoning before taking action."); - agent_config.withMaxIterations(5); - agent_config.withTemperature(0.3); - - // Create agent with loaded tools - auto agent = ReActAgent::create(llm_provider, registry, agent_config); - - // Store tools_fetcher and dispatcher in agent to keep connections alive - if (agent) { - agent->server_json_config_ = server_json_config; - agent->tools_loaded_ = true; - agent->tools_fetcher_ = std::move(tools_fetcher); - agent->owned_dispatcher_ = std::move(dispatcher); - } - - return agent; -} - -ReActAgent::Ptr ReActAgent::createByApiKey(const std::string& provider_name, - const std::string& model, - const std::string& api_key) { - // Fetch server configuration from remote API - std::string server_json_config; - try { - server_json_config = ApiEngine::fetchMcpServers(api_key); - } catch (const std::exception& e) { - // Failed to fetch server config - return nullptr; - } - - // Use the fetched config to create agent - return createByJson(provider_name, model, server_json_config); -} - -std::string ReActAgent::run(const std::string& query) { - // Lazy load tools on first run - if (!tools_loaded_) { - // Validate API response first - try { - JsonValue json = JsonValue::parse(server_json_config_); - - // Check if this is an API response format - if (json.contains("succeeded") && json.contains("message")) { - // Validate API response success - if (!json["succeeded"].getBool()) { - std::string message = json.contains("message") ? json["message"].getString() : "API request failed"; - return "Error: " + message; - } - - // Check message field for success status - std::string message = json["message"].getString(); - if (message != "success") { - return "Error: " + message; - } - } - } catch (const std::exception& e) { - return "Error: Invalid JSON configuration - " + std::string(e.what()); - } - - if (!loadTools()) { - return "Error: Failed to load MCP tools"; - } - tools_loaded_ = true; - } - - // Use the existing static run method with loaded configuration - return runWithLoadedAgent(query); -} - - -bool ReActAgent::loadTools() { - // Create dispatcher for loading tools (will be kept alive for agent lifetime) - auto dispatcher = std::make_unique("tools_loader"); - - // Create tools fetcher and load tools (will be kept alive for agent lifetime) - auto tools_fetcher = std::make_unique(); - - // Setup synchronization for async operations - std::mutex mtx; - std::condition_variable cv; - bool load_complete = false; - bool load_success = false; - - // Load tools from config - tools_fetcher->loadFromJson(server_json_config_, *dispatcher, - [&](VoidResult result) { - std::lock_guard lock(mtx); - load_complete = true; - load_success = mcp::holds_alternative(result); - cv.notify_one(); - }); - - // Wait for loading to complete - auto load_start = std::chrono::steady_clock::now(); - auto load_timeout = std::chrono::seconds(10); - - while (!load_complete) { - dispatcher->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (std::chrono::steady_clock::now() - load_start > load_timeout) { - return false; // Timeout - } - } - - if (load_success) { - auto registry = tools_fetcher->getRegistry(); - if (registry) { - // Update the agent's tools - impl_->tools = registry; - impl_->executor = makeToolExecutor(registry); - - // Store tools_fetcher and dispatcher to keep MCP connections alive - tools_fetcher_ = std::move(tools_fetcher); - owned_dispatcher_ = std::move(dispatcher); - - return true; - } - } - - return false; -} - -std::string ReActAgent::runWithLoadedAgent(const std::string& query) { - // Use the owned_dispatcher_ if available (for MCP connections created by createByJson) - // Otherwise create a static dispatcher for standalone usage - Dispatcher* dispatcher_to_use = nullptr; - - static thread_local std::unique_ptr static_dispatcher; - - if (owned_dispatcher_) { - // Use the dispatcher that owns the MCP connections - dispatcher_to_use = owned_dispatcher_.get(); - } else { - // Initialize static dispatcher on first use (per thread) for standalone agents - if (!static_dispatcher) { - static_dispatcher = std::make_unique("query_executor"); - } - dispatcher_to_use = static_dispatcher.get(); - } - - // Setup synchronization for async operations - std::mutex mtx; - std::condition_variable cv; - bool agent_complete = false; - std::string final_response; - - // Run agent and wait for completion - run(query, *dispatcher_to_use, - [&](Result result) { - std::lock_guard lock(mtx); - agent_complete = true; - if (mcp::holds_alternative(result)) { - auto response = mcp::get(result); - final_response = response.response; - } else { - final_response = "Error: " + mcp::get(result).message; - } - cv.notify_one(); - }); - - // Wait for agent completion with proper event loop handling - auto agent_start = std::chrono::steady_clock::now(); - auto agent_timeout = std::chrono::seconds(60); - - while (!agent_complete) { - // Process events on the dispatcher that owns MCP connections - dispatcher_to_use->run(mcp::event::RunType::NonBlock); - - // Short sleep to prevent busy waiting - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - - if (std::chrono::steady_clock::now() - agent_start > agent_timeout) { - // Cancel the agent before timing out - cancel(); - return "Error: Agent execution timed out after 60 seconds"; - } - } - - return final_response; -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/agent/agent_runnable.cc b/third_party/gopher-orch/src/gopher/orch/agent/agent_runnable.cc deleted file mode 100644 index c022d0dc..00000000 --- a/third_party/gopher-orch/src/gopher/orch/agent/agent_runnable.cc +++ /dev/null @@ -1,499 +0,0 @@ -// AgentRunnable Implementation - -#include "gopher/orch/agent/agent_runnable.h" - -#include - -namespace gopher { -namespace orch { -namespace agent { - -// ============================================================================= -// Factory Methods -// ============================================================================= - -AgentRunnable::Ptr AgentRunnable::create(LLMProviderPtr provider, - ToolExecutorPtr executor, - const AgentConfig& config) { - return Ptr( - new AgentRunnable(std::move(provider), std::move(executor), config)); -} - -AgentRunnable::Ptr AgentRunnable::create(LLMProviderPtr provider, - ToolRegistryPtr registry, - const AgentConfig& config) { - ToolExecutorPtr executor = registry ? makeToolExecutor(registry) : nullptr; - return create(std::move(provider), std::move(executor), config); -} - -AgentRunnable::Ptr AgentRunnable::create(LLMProviderPtr provider, - const AgentConfig& config) { - return create(std::move(provider), ToolExecutorPtr{}, config); -} - -AgentRunnable::AgentRunnable(LLMProviderPtr provider, - ToolExecutorPtr executor, - const AgentConfig& config) - : provider_(std::move(provider)), - executor_(std::move(executor)), - config_(config) {} - -// ============================================================================= -// Runnable Interface -// ============================================================================= - -std::string AgentRunnable::name() const { return "AgentRunnable"; } - -void AgentRunnable::invoke(const JsonValue& input, - const RunnableConfig& /* runnable_config */, - Dispatcher& dispatcher, - Callback callback) { - // Validate provider - if (!provider_) { - postError(dispatcher, std::move(callback), - AgentError::NO_PROVIDER, "No LLM provider configured"); - return; - } - - // Parse input - auto parsed = parseInput(input); - - if (parsed.query.empty() && parsed.context.empty()) { - postError(dispatcher, std::move(callback), - OrchError::INVALID_ARGUMENT, - "No query or messages provided"); - return; - } - - // Initialize state - AgentState state; - state.status = AgentStatus::RUNNING; - state.start_time = std::chrono::steady_clock::now(); - state.remaining_steps = parsed.config.max_iterations; - - // Add context messages - for (const auto& msg : parsed.context) { - state.messages.push_back(msg); - } - - // Add user query as message if provided - if (!parsed.query.empty()) { - state.messages.push_back(Message::user(parsed.query)); - } - - // Store config for this run - config_ = parsed.config; - - // Start the ReAct loop - executeLoop(state, dispatcher, std::move(callback)); -} - -// ============================================================================= -// Input Parsing -// ============================================================================= - -AgentRunnable::ParsedInput AgentRunnable::parseInput( - const JsonValue& input) const { - ParsedInput result; - result.config = config_; // Start with current config - - // Handle string input as simple query - if (input.isString()) { - result.query = input.getString(); - return result; - } - - if (!input.isObject()) { - return result; - } - - // Parse query - if (input.contains("query") && input["query"].isString()) { - result.query = input["query"].getString(); - } - - // Parse context messages - if (input.contains("context") && input["context"].isArray()) { - const auto& context_arr = input["context"]; - for (size_t i = 0; i < context_arr.size(); ++i) { - const auto& msg_json = context_arr[i]; - if (!msg_json.isObject()) - continue; - - Role role = Role::USER; - if (msg_json.contains("role") && msg_json["role"].isString()) { - role = parseRole(msg_json["role"].getString()); - } - - std::string content; - if (msg_json.contains("content") && msg_json["content"].isString()) { - content = msg_json["content"].getString(); - } - - result.context.push_back(Message(role, content)); - } - } - - // Parse LangGraph-style messages input - if (input.contains("messages") && input["messages"].isArray()) { - const auto& msgs_arr = input["messages"]; - for (size_t i = 0; i < msgs_arr.size(); ++i) { - const auto& msg_json = msgs_arr[i]; - if (!msg_json.isObject()) - continue; - - Role role = Role::USER; - if (msg_json.contains("role") && msg_json["role"].isString()) { - role = parseRole(msg_json["role"].getString()); - } - - std::string content; - if (msg_json.contains("content") && msg_json["content"].isString()) { - content = msg_json["content"].getString(); - } - - result.context.push_back(Message(role, content)); - } - } - - // Parse config overrides - if (input.contains("config") && input["config"].isObject()) { - const auto& cfg = input["config"]; - - if (cfg.contains("max_iterations") && cfg["max_iterations"].isNumber()) { - result.config.max_iterations = cfg["max_iterations"].getInt(); - } - if (cfg.contains("system_prompt") && cfg["system_prompt"].isString()) { - result.config.system_prompt = cfg["system_prompt"].getString(); - } - if (cfg.contains("model") && cfg["model"].isString()) { - result.config.llm_config.model = cfg["model"].getString(); - } - if (cfg.contains("temperature") && cfg["temperature"].isNumber()) { - result.config.llm_config.temperature = cfg["temperature"].getFloat(); - } - } - - return result; -} - -// ============================================================================= -// Agent Loop Execution -// ============================================================================= - -void AgentRunnable::executeLoop(AgentState& state, - Dispatcher& dispatcher, - Callback callback) { - // Check if should continue - if (!shouldContinue(state)) { - completeRun(state, std::move(callback)); - return; - } - - state.current_iteration++; - state.remaining_steps--; - - // Call LLM - callLLM(state, dispatcher, std::move(callback)); -} - -void AgentRunnable::callLLM(AgentState& state, - Dispatcher& dispatcher, - Callback callback) { - auto messages = buildMessages(state); - auto tools = getToolSpecs(); - - auto start_time = std::chrono::steady_clock::now(); - - // Capture state by value for the async callback - provider_->chat( - messages, tools, config_.llm_config, dispatcher, - [this, state, start_time, &dispatcher, - callback = std::move(callback)](Result result) mutable { - if (mcp::holds_alternative(result)) { - state.status = AgentStatus::FAILED; - state.error = mcp::get(result); - completeRun(state, std::move(callback)); - return; - } - - auto duration = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time); - - const auto& response = mcp::get(result); - - // Record step - recordStep(state, response.message, response.usage, duration); - - // Handle response - handleLLMResponse(response, state, dispatcher, std::move(callback)); - }); -} - -void AgentRunnable::handleLLMResponse(const LLMResponse& response, - AgentState& state, - Dispatcher& dispatcher, - Callback callback) { - // Add assistant message to history - state.messages.push_back(response.message); - - // Update usage - if (response.usage.has_value()) { - state.total_usage.prompt_tokens += response.usage->prompt_tokens; - state.total_usage.completion_tokens += response.usage->completion_tokens; - state.total_usage.total_tokens += response.usage->total_tokens; - } - - // Check if LLM wants to call tools - if (response.hasToolCalls()) { - executeTools(response.toolCalls(), state, dispatcher, std::move(callback)); - } else { - // No tool calls - agent is done - state.status = AgentStatus::COMPLETED; - completeRun(state, std::move(callback)); - } -} - -void AgentRunnable::executeTools(const std::vector& calls, - AgentState& state, - Dispatcher& dispatcher, - Callback callback) { - // Check tool approval - if (approval_callback_) { - for (const auto& call : calls) { - if (!approval_callback_(call)) { - state.status = AgentStatus::CANCELLED; - state.error = - Error(AgentError::CANCELLED, "Tool call rejected: " + call.name); - completeRun(state, std::move(callback)); - return; - } - } - } - - // Check if we have an executor - if (!executor_) { - // No tools - add error messages and continue - for (const auto& call : calls) { - state.messages.push_back( - Message::toolResult(call.id, "Error: No tools configured")); - } - // Continue loop to let LLM handle the error - dispatcher.post( - [this, state, &dispatcher, callback = std::move(callback)]() mutable { - executeLoop(state, dispatcher, std::move(callback)); - }); - return; - } - - // Execute tools - executor_->executeToolCalls( - calls, config_.parallel_tool_calls, dispatcher, - [this, calls, state, &dispatcher, callback = std::move(callback)]( - std::vector> results) mutable { - // Update last step with tool executions - if (!state.steps.empty()) { - auto& last_step = state.steps.back(); - for (size_t i = 0; i < calls.size(); ++i) { - ToolExecution exec; - exec.tool_name = calls[i].name; - exec.call_id = calls[i].id; - exec.input = calls[i].arguments; - - if (i < results.size()) { - if (mcp::holds_alternative(results[i])) { - exec.output = mcp::get(results[i]); - exec.success = true; - } else { - exec.success = false; - exec.error_message = mcp::get(results[i]).message; - } - } - - last_step.tool_executions.push_back(std::move(exec)); - } - } - - // Add tool results to messages - for (size_t i = 0; i < calls.size(); ++i) { - std::string result_content; - - if (i < results.size()) { - if (mcp::holds_alternative(results[i])) { - result_content = mcp::get(results[i]).toString(); - } else { - result_content = "Error: " + mcp::get(results[i]).message; - } - } else { - result_content = "Error: No result returned"; - } - - state.messages.push_back( - Message::toolResult(calls[i].id, result_content)); - } - - // Continue the loop - executeLoop(state, dispatcher, std::move(callback)); - }); -} - -void AgentRunnable::completeRun(AgentState& state, Callback callback) { - state.elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - state.start_time); - - // Check for max iterations - if (state.remaining_steps <= 0 && state.status == AgentStatus::RUNNING) { - state.status = AgentStatus::MAX_ITERATIONS_REACHED; - state.error = - Error(AgentError::MAX_ITERATIONS, "Maximum iterations reached"); - } - - // Build output - if (state.status == AgentStatus::COMPLETED || - state.status == AgentStatus::MAX_ITERATIONS_REACHED) { - JsonValue output = buildOutput(state); - callback(Result(std::move(output))); - } else { - // Return error - callback(Result( - state.error.value_or(Error(AgentError::UNKNOWN, "Unknown error")))); - } -} - -// ============================================================================= -// Output Building -// ============================================================================= - -JsonValue AgentRunnable::buildOutput(const AgentState& state) const { - JsonValue output = JsonValue::object(); - - // Get final response from last assistant message - std::string response; - for (auto it = state.messages.rbegin(); it != state.messages.rend(); ++it) { - if (it->role == Role::ASSISTANT && !it->content.empty()) { - response = it->content; - break; - } - } - output["response"] = response; - - // Status - output["status"] = agentStatusToString(state.status); - - // Iterations - output["iterations"] = static_cast(state.steps.size()); - - // Messages - JsonValue messages_arr = JsonValue::array(); - for (const auto& msg : state.messages) { - JsonValue msg_json = JsonValue::object(); - msg_json["role"] = roleToString(msg.role); - msg_json["content"] = msg.content; - if (msg.tool_call_id.has_value()) { - msg_json["tool_call_id"] = *msg.tool_call_id; - } - if (msg.hasToolCalls()) { - JsonValue calls_arr = JsonValue::array(); - for (const auto& call : *msg.tool_calls) { - JsonValue call_json = JsonValue::object(); - call_json["id"] = call.id; - call_json["name"] = call.name; - call_json["arguments"] = call.arguments; - calls_arr.push_back(call_json); - } - msg_json["tool_calls"] = calls_arr; - } - messages_arr.push_back(msg_json); - } - output["messages"] = messages_arr; - - // Usage - JsonValue usage = JsonValue::object(); - usage["prompt_tokens"] = state.total_usage.prompt_tokens; - usage["completion_tokens"] = state.total_usage.completion_tokens; - usage["total_tokens"] = state.total_usage.total_tokens; - output["usage"] = usage; - - // Duration - output["duration_ms"] = static_cast(state.elapsed.count()); - - // Error if any - if (state.error.has_value()) { - JsonValue error = JsonValue::object(); - error["code"] = state.error->code; - error["message"] = state.error->message; - output["error"] = error; - } - - return output; -} - -// ============================================================================= -// Helpers -// ============================================================================= - -std::vector AgentRunnable::buildMessages( - const AgentState& state) const { - std::vector messages; - - // Add system prompt if configured - if (!config_.system_prompt.empty()) { - messages.push_back(Message::system(config_.system_prompt)); - } - - // Add conversation history - for (const auto& msg : state.messages) { - messages.push_back(msg); - } - - return messages; -} - -std::vector AgentRunnable::getToolSpecs() const { - if (executor_ && executor_->registry()) { - return executor_->registry()->getToolSpecs(); - } - return {}; -} - -bool AgentRunnable::shouldContinue(const AgentState& state) const { - // Stop if not running - if (state.status != AgentStatus::RUNNING) { - return false; - } - - // Stop if max iterations reached - if (state.remaining_steps <= 0) { - return false; - } - - // Check timeout - auto elapsed = std::chrono::steady_clock::now() - state.start_time; - if (elapsed > config_.timeout) { - return false; - } - - return true; -} - -void AgentRunnable::recordStep(AgentState& state, - const Message& llm_message, - const optional& usage, - std::chrono::milliseconds llm_duration) { - AgentStep step; - step.step_number = state.current_iteration; - step.llm_message = llm_message; - step.llm_usage = usage; - step.llm_duration = llm_duration; - - state.steps.push_back(std::move(step)); - - // Invoke step callback - if (step_callback_ && config_.enable_step_callbacks) { - step_callback_(state.steps.back()); - } -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/agent/api_engine.cc b/third_party/gopher-orch/src/gopher/orch/agent/api_engine.cc deleted file mode 100644 index 43916f67..00000000 --- a/third_party/gopher-orch/src/gopher/orch/agent/api_engine.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "gopher/orch/agent/api_engine.h" - -#include - -#include "gopher/orch/server/rest_server.h" - -namespace gopher { -namespace orch { -namespace agent { - -using namespace gopher::orch::core; -using namespace gopher::orch::server; - -std::string ApiEngine::fetchMcpServers(const std::string& apiKey) { - // Validate API key - if (apiKey.empty()) { - throw std::runtime_error("API key is required"); - } - - // Construct the URL - std::string url = getApiUrlRoot() + "/v1/mcp-servers"; - - // Set up headers - std::map headers; - headers["Content-Type"] = "application/json"; - headers["X-API-Key"] = apiKey; - - // Make the HTTP request using the utility function - return fetchJsonSync(url, headers); -} - -} // namespace agent -} // namespace orch -} // namespace gopher \ No newline at end of file diff --git a/third_party/gopher-orch/src/gopher/orch/agent/config_loader.cc b/third_party/gopher-orch/src/gopher/orch/agent/config_loader.cc deleted file mode 100644 index b0ef2209..00000000 --- a/third_party/gopher-orch/src/gopher/orch/agent/config_loader.cc +++ /dev/null @@ -1,75 +0,0 @@ -// ConfigLoader Implementation - File I/O operations - -#include "gopher/orch/agent/config_loader.h" - -#include -#include - -namespace gopher { -namespace orch { -namespace agent { - -VoidResult ConfigLoader::loadEnvFile(const std::string& path) { - std::ifstream file(path); - if (!file.is_open()) { - return VoidResult(Error(-1, "Cannot open .env file: " + path)); - } - - std::string line; - while (std::getline(file, line)) { - // Skip empty lines and comments - if (line.empty() || line[0] == '#') { - continue; - } - - // Find the = separator - auto pos = line.find('='); - if (pos == std::string::npos) { - continue; - } - - std::string key = line.substr(0, pos); - std::string value = line.substr(pos + 1); - - // Trim whitespace - while (!key.empty() && std::isspace(key.back())) - key.pop_back(); - while (!key.empty() && std::isspace(key.front())) - key.erase(0, 1); - while (!value.empty() && std::isspace(value.back())) - value.pop_back(); - while (!value.empty() && std::isspace(value.front())) - value.erase(0, 1); - - // Remove quotes if present - if (value.size() >= 2) { - if ((value.front() == '"' && value.back() == '"') || - (value.front() == '\'' && value.back() == '\'')) { - value = value.substr(1, value.size() - 2); - } - } - - if (!key.empty()) { - env_vars_[key] = value; - } - } - - return VoidResult(nullptr); -} - -Result ConfigLoader::loadFromFile(const std::string& path) { - std::ifstream file(path); - if (!file.is_open()) { - return Result( - Error(-1, "Cannot open config file: " + path)); - } - - std::stringstream buffer; - buffer << file.rdbuf(); - - return loadFromString(buffer.str()); -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/agent/tool_registry.cc b/third_party/gopher-orch/src/gopher/orch/agent/tool_registry.cc deleted file mode 100644 index 9d2b7cae..00000000 --- a/third_party/gopher-orch/src/gopher/orch/agent/tool_registry.cc +++ /dev/null @@ -1,323 +0,0 @@ -// ToolRegistry Config Loading Implementation - -#include -#include "gopher/orch/agent/tool_registry.h" - -#include "gopher/orch/agent/config_loader.h" -#include "gopher/orch/agent/rest_tool_adapter.h" -#include "gopher/orch/agent/tool_definition.h" - -#ifdef GOPHER_ORCH_WITH_MCP -#include "gopher/orch/server/mcp_server.h" -#endif - -namespace gopher { -namespace orch { -namespace agent { - -// ═══════════════════════════════════════════════════════════════════════════ -// CONFIG LOADING -// ═══════════════════════════════════════════════════════════════════════════ - -void ToolRegistry::loadFromFile(const std::string& path, - Dispatcher& dispatcher, - std::function callback) { - ConfigLoader loader; - - // Copy env vars to loader - { - std::lock_guard lock(mutex_); - for (const auto& kv : env_vars_) { - loader.setEnv(kv.first, kv.second); - } - } - - auto result = loader.loadFromFile(path); - if (!mcp::holds_alternative(result)) { - dispatcher.post( - [callback = std::move(callback), err = mcp::get(result)]() { - callback(VoidResult(err)); - }); - return; - } - - loadConfig(mcp::get(result), dispatcher, std::move(callback)); -} - -void ToolRegistry::loadFromString(const std::string& json_string, - Dispatcher& dispatcher, - std::function callback) { - ConfigLoader loader; - - { - std::lock_guard lock(mutex_); - for (const auto& kv : env_vars_) { - loader.setEnv(kv.first, kv.second); - } - } - - auto result = loader.loadFromString(json_string); - if (!mcp::holds_alternative(result)) { - dispatcher.post( - [callback = std::move(callback), err = mcp::get(result)]() { - callback(VoidResult(err)); - }); - return; - } - - loadConfig(mcp::get(result), dispatcher, std::move(callback)); -} - -void ToolRegistry::loadConfig(const RegistryConfig& config, - Dispatcher& dispatcher, - std::function callback) { - // Track pending MCP server connections - auto pending = - std::make_shared>(config.mcp_servers.size()); - auto errors = std::make_shared>(); - auto self = this; - auto config_copy = std::make_shared(config); - - auto on_all_connected = [self, config_copy, callback, errors, - &dispatcher]() mutable { - // Register tools after all MCP servers connected - for (const auto& tool_def : config_copy->tools) { - auto result = self->registerTool(tool_def, dispatcher); - if (!mcp::holds_alternative(result)) { - errors->push_back("Tool " + tool_def.name + ": " + - mcp::get(result).message); - } - } - - if (!errors->empty()) { - std::string error_msg = "Errors during config load:"; - for (const auto& e : *errors) { - error_msg += "\n - " + e; - } - callback(VoidResult(Error(-1, error_msg))); - } else { - callback(VoidResult(nullptr)); - } - }; - - if (config.mcp_servers.empty()) { - dispatcher.post([on_all_connected]() mutable { on_all_connected(); }); - return; - } - - // Connect to MCP servers - for (const auto& server_def : config.mcp_servers) { - addMCPServer(server_def, dispatcher, - [pending, errors, on_all_connected, - name = server_def.name](VoidResult result) mutable { - if (!mcp::holds_alternative(result)) { - errors->push_back("MCP server " + name + ": " + - mcp::get(result).message); - } - - if (--(*pending) == 0) { - on_all_connected(); - } - }); - } -} - -VoidResult ToolRegistry::registerTool(const ToolDefinition& def, - Dispatcher& dispatcher) { - // Create ToolEntry from definition - ToolEntry entry; - entry.spec = def.toToolSpec(); - - // Handle different tool types - if (def.handler) { - // Lambda handler - entry.function = *def.handler; - } else if (def.rest_endpoint) { - // REST endpoint - create adapter - auto adapter = std::make_shared(); - - // Copy env vars - { - std::lock_guard lock(mutex_); - for (const auto& kv : env_vars_) { - adapter->setEnv(kv.first, kv.second); - } - } - - entry.function = adapter->createToolFunction(def); - if (!entry.function) { - return VoidResult(Error(-1, "Failed to create REST tool: " + def.name)); - } - } else if (def.mcp_reference) { - // MCP reference - proxy to MCP server - const auto& ref = *def.mcp_reference; - ServerPtr server = getMCPServer(ref.server_name); - - if (server) { - entry.server = server; - entry.original_name = ref.tool_name; - } else { - return VoidResult(Error(-1, "MCP server not found: " + ref.server_name)); - } - } else { - return VoidResult(Error( - -1, - "Tool has no handler, REST endpoint, or MCP reference: " + def.name)); - } - - // Register the tool - { - std::lock_guard lock(mutex_); - tools_[def.name] = std::move(entry); - } - - return VoidResult(nullptr); -} - -VoidResult ToolRegistry::loadEnvFile(const std::string& path) { - ConfigLoader loader; - auto result = loader.loadEnvFile(path); - if (!mcp::holds_alternative(result)) { - return result; - } - - // Note: The loader only loads to its internal state - // We need to read the file directly here - std::ifstream file(path); - if (!file.is_open()) { - return VoidResult(Error(-1, "Cannot open .env file: " + path)); - } - - std::string line; - while (std::getline(file, line)) { - if (line.empty() || line[0] == '#') - continue; - - auto pos = line.find('='); - if (pos == std::string::npos) - continue; - - std::string key = line.substr(0, pos); - std::string value = line.substr(pos + 1); - - // Trim - while (!key.empty() && std::isspace(key.back())) - key.pop_back(); - while (!key.empty() && std::isspace(key.front())) - key.erase(0, 1); - while (!value.empty() && std::isspace(value.back())) - value.pop_back(); - while (!value.empty() && std::isspace(value.front())) - value.erase(0, 1); - - // Remove quotes - if (value.size() >= 2) { - if ((value.front() == '"' && value.back() == '"') || - (value.front() == '\'' && value.back() == '\'')) { - value = value.substr(1, value.size() - 2); - } - } - - if (!key.empty()) { - setEnv(key, value); - } - } - - return VoidResult(nullptr); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// MCP SERVER MANAGEMENT -// ═══════════════════════════════════════════════════════════════════════════ - -void ToolRegistry::addMCPServer(const MCPServerDefinition& def, - Dispatcher& dispatcher, - std::function callback) { -#ifdef GOPHER_ORCH_WITH_MCP - using namespace gopher::orch::server; - - MCPServerConfig config; - config.name = def.name; - config.connect_timeout = def.connect_timeout; - config.request_timeout = def.request_timeout; - config.max_connect_retries = def.max_retries; - - // Configure transport - switch (def.transport) { - case MCPServerDefinition::TransportType::STDIO: { - if (!def.stdio_config) { - dispatcher.post([callback = std::move(callback)]() { - callback(VoidResult(Error(-1, "STDIO config missing"))); - }); - return; - } - config.transport_type = MCPServerConfig::TransportType::STDIO; - config.stdio_transport.command = def.stdio_config->command; - config.stdio_transport.args = def.stdio_config->args; - config.stdio_transport.env = def.stdio_config->env; - config.stdio_transport.working_directory = - def.stdio_config->working_directory; - break; - } - - case MCPServerDefinition::TransportType::HTTP_SSE: { - if (!def.http_sse_config) { - dispatcher.post([callback = std::move(callback)]() { - callback(VoidResult(Error(-1, "HTTP-SSE config missing"))); - }); - return; - } - config.transport_type = MCPServerConfig::TransportType::HTTP_SSE; - config.http_sse_transport.url = def.http_sse_config->url; - config.http_sse_transport.headers = def.http_sse_config->headers; - config.http_sse_transport.verify_ssl = def.http_sse_config->verify_ssl; - break; - } - - case MCPServerDefinition::TransportType::WEBSOCKET: { - if (!def.websocket_config) { - dispatcher.post([callback = std::move(callback)]() { - callback(VoidResult(Error(-1, "WebSocket config missing"))); - }); - return; - } - config.transport_type = MCPServerConfig::TransportType::WEBSOCKET; - config.websocket_transport.url = def.websocket_config->url; - config.websocket_transport.headers = def.websocket_config->headers; - config.websocket_transport.verify_ssl = def.websocket_config->verify_ssl; - break; - } - } - - // Create and connect MCP server - MCPServer::create(config, dispatcher, - [this, name = def.name, callback = std::move(callback)]( - Result result) { - if (!mcp::holds_alternative(result)) { - callback(VoidResult(mcp::get(result))); - return; - } - - auto server = mcp::get(result); - - // Store in registry - { - std::lock_guard lock(mutex_); - mcp_servers_[name] = server; - servers_.push_back(server); - } - - callback(VoidResult(nullptr)); - }); -#else - // MCP not available - dispatcher.post([callback = std::move(callback)]() { - callback(VoidResult(Error( - -1, "MCP support not compiled (GOPHER_ORCH_WITH_MCP not defined)"))); - }); -#endif -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/agent/tool_runnable.cc b/third_party/gopher-orch/src/gopher/orch/agent/tool_runnable.cc deleted file mode 100644 index 851825ab..00000000 --- a/third_party/gopher-orch/src/gopher/orch/agent/tool_runnable.cc +++ /dev/null @@ -1,213 +0,0 @@ -// ToolRunnable Implementation - -#include "gopher/orch/agent/tool_runnable.h" - -#include - -namespace gopher { -namespace orch { -namespace agent { - -// ============================================================================= -// Factory -// ============================================================================= - -ToolRunnable::Ptr ToolRunnable::create(ToolExecutorPtr executor) { - return Ptr(new ToolRunnable(std::move(executor))); -} - -ToolRunnable::ToolRunnable(ToolExecutorPtr executor) - : executor_(std::move(executor)) {} - -// ============================================================================= -// Runnable Interface -// ============================================================================= - -std::string ToolRunnable::name() const { return "ToolRunnable"; } - -void ToolRunnable::invoke(const JsonValue& input, - const RunnableConfig& /* config */, - Dispatcher& dispatcher, - Callback callback) { - // Validate executor - if (!executor_) { - postError(dispatcher, std::move(callback), - OrchError::INVALID_ARGUMENT, - "No tool executor configured"); - return; - } - - // Check if input has tool_calls array (multiple calls) - if (input.isObject() && input.contains("tool_calls") && - input["tool_calls"].isArray()) { - auto calls = parseMultipleCalls(input); - if (calls.empty()) { - postError(dispatcher, std::move(callback), - OrchError::INVALID_ARGUMENT, - "Empty tool_calls array"); - return; - } - executeMultiple(calls, dispatcher, std::move(callback)); - return; - } - - // Single tool call - auto single = parseSingleCall(input); - if (!single.valid) { - postError(dispatcher, std::move(callback), - OrchError::INVALID_ARGUMENT, - "Invalid tool call input: missing 'name' field"); - return; - } - - executeSingle(single.id, single.name, single.arguments, dispatcher, - std::move(callback)); -} - -// ============================================================================= -// Execution -// ============================================================================= - -void ToolRunnable::executeSingle(const std::string& id, - const std::string& name, - const JsonValue& arguments, - Dispatcher& dispatcher, - Callback callback) { - executor_->executeTool( - name, arguments, dispatcher, - [id, callback = std::move(callback)](Result result) mutable { - JsonValue output = JsonValue::object(); - if (!id.empty()) { - output["id"] = id; - } - - if (mcp::holds_alternative(result)) { - output["success"] = false; - output["error"] = mcp::get(result).message; - // Still return success Result with error info in JSON - callback(Result(std::move(output))); - } else { - output["success"] = true; - output["result"] = mcp::get(result); - callback(Result(std::move(output))); - } - }); -} - -void ToolRunnable::executeMultiple(const std::vector& calls, - Dispatcher& dispatcher, - Callback callback) { - // Use the executor's parallel execution - executor_->executeToolCalls( - calls, true, // parallel = true - dispatcher, - [calls, callback = std::move(callback)]( - std::vector> results) mutable { - JsonValue output = JsonValue::object(); - JsonValue results_array = JsonValue::array(); - - for (size_t i = 0; i < calls.size(); ++i) { - JsonValue result_obj = JsonValue::object(); - result_obj["id"] = calls[i].id; - - if (i < results.size()) { - if (mcp::holds_alternative(results[i])) { - result_obj["success"] = true; - result_obj["result"] = mcp::get(results[i]); - } else { - result_obj["success"] = false; - result_obj["error"] = mcp::get(results[i]).message; - } - } else { - result_obj["success"] = false; - result_obj["error"] = "No result returned"; - } - - results_array.push_back(result_obj); - } - - output["results"] = results_array; - callback(Result(std::move(output))); - }); -} - -// ============================================================================= -// Parsing -// ============================================================================= - -ToolRunnable::SingleCall ToolRunnable::parseSingleCall(const JsonValue& input) { - SingleCall result; - - if (!input.isObject()) { - return result; - } - - // Get name (required) - if (input.contains("name") && input["name"].isString()) { - result.name = input["name"].getString(); - result.valid = true; - } else { - return result; - } - - // Get id (optional) - if (input.contains("id") && input["id"].isString()) { - result.id = input["id"].getString(); - } - - // Get arguments (optional, default to empty object) - if (input.contains("arguments")) { - result.arguments = input["arguments"]; - } else { - result.arguments = JsonValue::object(); - } - - return result; -} - -std::vector ToolRunnable::parseMultipleCalls(const JsonValue& input) { - std::vector calls; - - if (!input.isObject() || !input.contains("tool_calls") || - !input["tool_calls"].isArray()) { - return calls; - } - - const auto& calls_array = input["tool_calls"]; - for (size_t i = 0; i < calls_array.size(); ++i) { - const auto& call_obj = calls_array[i]; - if (!call_obj.isObject()) { - continue; - } - - ToolCall call; - - // Get name (required) - if (!call_obj.contains("name") || !call_obj["name"].isString()) { - continue; - } - call.name = call_obj["name"].getString(); - - // Get id (optional, generate if missing) - if (call_obj.contains("id") && call_obj["id"].isString()) { - call.id = call_obj["id"].getString(); - } else { - call.id = "call_" + std::to_string(i); - } - - // Get arguments - if (call_obj.contains("arguments")) { - call.arguments = call_obj["arguments"]; - } else { - call.arguments = JsonValue::object(); - } - - calls.push_back(std::move(call)); - } - - return calls; -} - -} // namespace agent -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/agent/tools_fetcher.cpp b/third_party/gopher-orch/src/gopher/orch/agent/tools_fetcher.cpp deleted file mode 100644 index 844b9122..00000000 --- a/third_party/gopher-orch/src/gopher/orch/agent/tools_fetcher.cpp +++ /dev/null @@ -1,197 +0,0 @@ -/** - * @file tools_fetcher.cpp - * @brief Implementation of ToolsFetcher orchestration layer - */ - -#include "gopher/orch/agent/tools_fetcher.h" - -#include -#include -#include -#include - -#include "gopher/orch/agent/config_loader.h" -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/server/server_composite.h" - -#ifdef GOPHER_ORCH_WITH_MCP -#include "gopher/orch/server/mcp_server.h" -#endif - -namespace gopher { -namespace orch { -namespace agent { - -void ToolsFetcher::loadFromJson(const std::string& json_config, - Dispatcher& dispatcher, - std::function callback) { - // Initialize ConfigLoader if needed - if (!config_loader_) { - config_loader_ = std::make_shared(); - } - - // Parse JSON configuration - auto config_result = config_loader_->loadFromString(json_config); - if (!mcp::holds_alternative(config_result)) { - dispatcher.post([callback, err = mcp::get(config_result)]() { - callback(VoidResult(err)); - }); - return; - } - - auto config = mcp::get(config_result); - -#ifdef GOPHER_ORCH_WITH_MCP - // Create ServerComposite - composite_ = server::ServerComposite::create("ToolComposite"); - - // Handle no servers case - if (config.mcp_servers.empty()) { - // Create empty ToolRegistry with composite - registry_ = std::make_shared(); - registry_->setServerComposite(composite_); - dispatcher.post([callback]() { - callback(VoidResult(nullptr)); - }); - return; - } - - // Track pending server connections - auto pending = std::make_shared>(config.mcp_servers.size()); - auto errors = std::make_shared>(); - auto servers = std::make_shared>>(); - - // CRITICAL FIX: Capture dispatcher by pointer instead of reference to avoid - // dangling reference when lambdas are invoked asynchronously after this - // function returns. The dispatcher is owned by the caller and must outlive - // all async operations. - Dispatcher* dispatcher_ptr = &dispatcher; - - // Create completion handler - auto on_all_connected = [this, dispatcher_ptr, callback, errors, servers, pending]() { - if (!errors->empty()) { - std::string error_msg = "Server connection errors:"; - for (const auto& e : *errors) { - error_msg += "\n - " + e; - } - callback(VoidResult(Error(-1, error_msg))); - return; - } - - // Create ToolRegistry with composite delegation - registry_ = std::make_shared(); - registry_->setServerComposite(this->composite_); - - // Check if we have any servers to discover tools from - if (servers->empty()) { - // No servers connected successfully, but still call callback with success - // The client can check registry->toolCount() to know no tools were discovered - dispatcher_ptr->post([callback]() { - callback(VoidResult(nullptr)); - }); - return; - } - - // Track pending tool discoveries (registry->addServer now delegates to composite) - auto discovery_pending = std::make_shared>(servers->size()); - auto discovery_complete = [callback, discovery_pending, dispatcher_ptr, registry = this->registry_]() { - if (--(*discovery_pending) == 0) { - // All discoveries complete - add a final dispatch to ensure registry is updated - dispatcher_ptr->post([callback, registry, dispatcher_ptr]() { - // Give one more event loop cycle for registry updates to complete - dispatcher_ptr->post([callback]() { - callback(VoidResult(nullptr)); - }); - }); - } - }; - - // Add all servers to registry (which now delegates to composite internally) - for (const auto& server_pair : *servers) { - const server::MCPServerPtr& server = server_pair.second; - - // addServer() now handles both registry and composite registration - this->registry_->addServer(server, *dispatcher_ptr); - discovery_complete(); - } - }; - - // Connect to each MCP server - for (const auto& server_def : config.mcp_servers) { - // Convert to MCPServerConfig - server::MCPServerConfig mcp_config; - mcp_config.name = server_def.name; - mcp_config.connect_timeout = std::chrono::milliseconds(server_def.connect_timeout); - mcp_config.request_timeout = std::chrono::milliseconds(server_def.request_timeout); - - // Configure transport - if (server_def.transport == MCPServerDefinition::TransportType::HTTP_SSE && - server_def.http_sse_config) { - mcp_config.transport_type = server::MCPServerConfig::TransportType::HTTP_SSE; - mcp_config.http_sse_transport.url = server_def.http_sse_config->url; - mcp_config.http_sse_transport.headers = server_def.http_sse_config->headers; - } - - // Track completion for this server - auto server_name = server_def.name; - - // Create and connect to the server - server::MCPServer::create(mcp_config, dispatcher, - [servers, errors, pending, on_all_connected, server_name] - (Result result) { - - if (mcp::holds_alternative(result)) { - servers->push_back({server_name, mcp::get(result)}); - } else { - errors->push_back(server_name + ": " + mcp::get(result).message); - } - - if (--(*pending) == 0) { - on_all_connected(); - } - }); - } -#else - // MCP not available - dispatcher.post([callback]() { - callback(VoidResult(Error(-1, "MCP support not compiled in"))); - }); -#endif -} - -void ToolsFetcher::loadFromFile(const std::string& file_path, - Dispatcher& dispatcher, - std::function callback) { - // Read file contents - std::ifstream file(file_path); - if (!file.is_open()) { - dispatcher.post([callback, file_path]() { - callback(VoidResult(Error(-1, "Cannot open file: " + file_path))); - }); - return; - } - - std::stringstream buffer; - buffer << file.rdbuf(); - file.close(); - - // Call loadFromJson with file contents - loadFromJson(buffer.str(), dispatcher, callback); -} - -void ToolsFetcher::shutdown(Dispatcher& dispatcher, - std::function callback) { -#ifdef GOPHER_ORCH_WITH_MCP - if (composite_) { - composite_->disconnectAll(dispatcher, std::move(callback)); - } else { - dispatcher.post(std::move(callback)); - } -#else - dispatcher.post(std::move(callback)); -#endif -} - -} // namespace agent -} // namespace orch -} // namespace gopher \ No newline at end of file diff --git a/third_party/gopher-orch/src/gopher/orch/ffi/orch_ffi_agent.cc b/third_party/gopher-orch/src/gopher/orch/ffi/orch_ffi_agent.cc deleted file mode 100644 index 1b7d2fb4..00000000 --- a/third_party/gopher-orch/src/gopher/orch/ffi/orch_ffi_agent.cc +++ /dev/null @@ -1,252 +0,0 @@ -/** - * @file orch_ffi_agent.cc - * @brief FFI implementation for Agent API functions - * - * This file implements the C API functions for agent functionality, - * providing FFI-safe wrappers around the C++ ReActAgent class. - */ - -#include "gopher/orch/ffi/orch_ffi.h" -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/agent/agent.h" -#include "gopher/orch/agent/api_engine.h" -#include - -using namespace gopher::orch::ffi; -using namespace gopher::orch::agent; - -extern "C" { - -/* ============================================================================ - * Helper Functions - * ============================================================================ - */ - -/** - * Validate handle and cast to AgentImpl - */ -static AgentImpl* ValidateAgentHandle(gopher_orch_agent_t handle) { - if (!handle) { - SET_ERROR(GOPHER_ORCH_ERROR_NULL_POINTER, "Agent handle is NULL"); - return nullptr; - } - - if (!HandleRegistry::Instance().IsValid(handle)) { - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_HANDLE, "Agent handle is invalid"); - return nullptr; - } - - auto* impl = reinterpret_cast(handle); - if (impl->GetType() != GOPHER_ORCH_TYPE_AGENT) { - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_ARGUMENT, "Handle is not an agent"); - return nullptr; - } - - return impl; -} - -/** - * Copy string to C-style allocated memory - */ -static char* AllocateString(const std::string& str) { - char* result = static_cast(malloc(str.length() + 1)); - if (result) { - std::strcpy(result, str.c_str()); - } - return result; -} - -/* ============================================================================ - * Agent API Implementation - * ============================================================================ - */ - -GOPHER_ORCH_API gopher_orch_agent_t gopher_orch_agent_create_by_json( - const char* provider_name, - const char* model_name, - const char* server_json_config) GOPHER_ORCH_NOEXCEPT { - - ErrorManager::ClearError(); - - try { - // Validate parameters - if (!provider_name || !model_name || !server_json_config) { - SET_ERROR(GOPHER_ORCH_ERROR_NULL_POINTER, "Required parameter is NULL"); - return nullptr; - } - - // Create agent using the existing C++ API - auto agent = ReActAgent::createByJson( - std::string(provider_name), - std::string(model_name), - std::string(server_json_config) - ); - - if (!agent) { - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_ARGUMENT, - "Failed to create agent from JSON configuration"); - return nullptr; - } - - // Wrap in FFI handle - auto* impl = new AgentImpl(agent); - return reinterpret_cast(impl); - - } catch (const std::exception& e) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - std::string("Exception in agent creation: ") + e.what()); - return nullptr; - } catch (...) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - "Unknown exception in agent creation"); - return nullptr; - } -} - -GOPHER_ORCH_API gopher_orch_agent_t gopher_orch_agent_create_by_api_key( - const char* provider_name, - const char* model_name, - const char* api_key) GOPHER_ORCH_NOEXCEPT { - - ErrorManager::ClearError(); - - try { - // Validate parameters - if (!provider_name || !model_name || !api_key) { - SET_ERROR(GOPHER_ORCH_ERROR_NULL_POINTER, "Required parameter is NULL"); - return nullptr; - } - - // Create agent using the existing C++ API - auto agent = ReActAgent::createByApiKey( - std::string(provider_name), - std::string(model_name), - std::string(api_key) - ); - - if (!agent) { - SET_ERROR(GOPHER_ORCH_ERROR_INVALID_ARGUMENT, - "Failed to create agent with API key"); - return nullptr; - } - - // Wrap in FFI handle - auto* impl = new AgentImpl(agent); - return reinterpret_cast(impl); - - } catch (const std::exception& e) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - std::string("Exception in agent creation: ") + e.what()); - return nullptr; - } catch (...) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - "Unknown exception in agent creation"); - return nullptr; - } -} - -GOPHER_ORCH_API char* gopher_orch_agent_run( - gopher_orch_agent_t agent, - const char* query, - uint64_t timeout_ms) GOPHER_ORCH_NOEXCEPT { - - ErrorManager::ClearError(); - - try { - // Validate agent handle - auto* impl = ValidateAgentHandle(agent); - if (!impl) { - return nullptr; // Error already set by ValidateAgentHandle - } - - // Validate query - if (!query) { - SET_ERROR(GOPHER_ORCH_ERROR_NULL_POINTER, "Query is NULL"); - return nullptr; - } - - // Run the agent - // Note: The current ReActAgent::run() method doesn't support timeout parameter - // TODO: Add timeout support to the C++ API if needed - std::string response = impl->agent->run(std::string(query)); - - // Allocate and return response string - return AllocateString(response); - - } catch (const std::exception& e) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - std::string("Exception in agent run: ") + e.what()); - return nullptr; - } catch (...) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - "Unknown exception in agent run"); - return nullptr; - } -} - -GOPHER_ORCH_API void gopher_orch_agent_add_ref( - gopher_orch_agent_t agent) GOPHER_ORCH_NOEXCEPT { - - if (auto* impl = ValidateAgentHandle(agent)) { - impl->AddRef(); - } -} - -GOPHER_ORCH_API void gopher_orch_agent_release( - gopher_orch_agent_t agent) GOPHER_ORCH_NOEXCEPT { - - if (auto* impl = ValidateAgentHandle(agent)) { - impl->Release(); - } -} - -GOPHER_ORCH_API char* gopher_orch_api_fetch_servers( - const char* api_key) GOPHER_ORCH_NOEXCEPT { - - ErrorManager::ClearError(); - - try { - // Validate parameter - if (!api_key) { - SET_ERROR(GOPHER_ORCH_ERROR_NULL_POINTER, "API key is NULL"); - return nullptr; - } - - // Fetch server configuration using the existing C++ API - std::string config = ApiEngine::fetchMcpServers(std::string(api_key)); - - // Allocate and return configuration string - return AllocateString(config); - - } catch (const std::exception& e) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - std::string("Exception in API fetch: ") + e.what()); - return nullptr; - } catch (...) { - SET_ERROR(GOPHER_ORCH_ERROR_INTERNAL, - "Unknown exception in API fetch"); - return nullptr; - } -} - -/* ============================================================================ - * Error Handling Functions - * ============================================================================ - */ - -GOPHER_ORCH_API const gopher_orch_error_info_t* gopher_orch_last_error(void) - GOPHER_ORCH_NOEXCEPT { - return ErrorManager::GetLastError(); -} - -GOPHER_ORCH_API void gopher_orch_clear_error(void) GOPHER_ORCH_NOEXCEPT { - ErrorManager::ClearError(); -} - -GOPHER_ORCH_API void gopher_orch_free(void* ptr) GOPHER_ORCH_NOEXCEPT { - if (ptr) { - free(ptr); - } -} - -} /* extern "C" */ \ No newline at end of file diff --git a/third_party/gopher-orch/src/gopher/orch/llm/anthropic_provider.cc b/third_party/gopher-orch/src/gopher/orch/llm/anthropic_provider.cc deleted file mode 100644 index b8dfb31f..00000000 --- a/third_party/gopher-orch/src/gopher/orch/llm/anthropic_provider.cc +++ /dev/null @@ -1,464 +0,0 @@ -// Anthropic Provider Implementation - -#include "gopher/orch/llm/anthropic_provider.h" - -#include -#include -#include -#include - -#include "gopher/orch/server/rest_server.h" - -namespace gopher { -namespace orch { -namespace llm { - -using namespace gopher::orch::core; -using namespace gopher::orch::server; - -// ═══════════════════════════════════════════════════════════════════════════ -// IMPLEMENTATION -// ═══════════════════════════════════════════════════════════════════════════ - -class AnthropicProvider::Impl { - public: - AnthropicConfig config; - HttpClientPtr http_client; - mutable std::mutex mutex; - - explicit Impl(const AnthropicConfig& cfg) : config(cfg) { - // Use CurlHttpClient for real HTTP requests - http_client = server::createCurlHttpClient(); - } - - std::string messagesEndpoint() const { - return config.base_url + "/v1/messages"; - } - - std::map headers() const { - std::map hdrs; - hdrs["Content-Type"] = "application/json"; - hdrs["x-api-key"] = config.api_key; - hdrs["anthropic-version"] = config.api_version; - - // Add beta headers if any - if (!config.betas.empty()) { - std::string beta_str; - for (size_t i = 0; i < config.betas.size(); ++i) { - if (i > 0) - beta_str += ","; - beta_str += config.betas[i]; - } - hdrs["anthropic-beta"] = beta_str; - } - - return hdrs; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// FACTORY METHODS -// ═══════════════════════════════════════════════════════════════════════════ - -namespace { -// Helper to get API key from parameter or environment variable -std::string resolveApiKey(const std::string& api_key) { - if (!api_key.empty()) { - return api_key; - } - - // Try to get from environment variable - const char* env_key = std::getenv("ANTHROPIC_API_KEY"); - if (env_key && env_key[0] != '\0') { - return std::string(env_key); - } - - return ""; -} -} // namespace - -AnthropicProvider::Ptr AnthropicProvider::create(const std::string& api_key) { - return create(AnthropicConfig(api_key)); -} - -AnthropicProvider::Ptr AnthropicProvider::create(const std::string& api_key, - const std::string& base_url) { - AnthropicConfig config(api_key); - if (!base_url.empty()) { - config.withBaseUrl(base_url); - } - return create(config); -} - -AnthropicProvider::Ptr AnthropicProvider::create( - const AnthropicConfig& config) { - // Resolve API key from config or environment variable - std::string resolved_key = resolveApiKey(config.api_key); - - if (resolved_key.empty()) { - throw std::runtime_error( - "ANTHROPIC_API_KEY not set. Please set the ANTHROPIC_API_KEY " - "environment variable or pass the API key directly."); - } - - // Create config with resolved key - AnthropicConfig resolved_config = config; - resolved_config.api_key = resolved_key; - - return Ptr(new AnthropicProvider(resolved_config)); -} - -AnthropicProvider::AnthropicProvider(const AnthropicConfig& config) - : impl_(std::make_unique(config)) {} - -AnthropicProvider::~AnthropicProvider() = default; - -// ═══════════════════════════════════════════════════════════════════════════ -// CHAT COMPLETION -// ═══════════════════════════════════════════════════════════════════════════ - -void AnthropicProvider::chat(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) { - auto request = buildRequest(messages, tools, config, false); - auto request_body = request.toString(); - - auto url = impl_->messagesEndpoint(); - auto headers = impl_->headers(); - - impl_->http_client->request( - HttpMethod::POST, url, headers, request_body, dispatcher, - [this, callback = std::move(callback)](Result result) { - if (!mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - return; - } - - auto& response = mcp::get(result); - if (!response.isSuccess()) { - std::string error_msg = - "HTTP " + std::to_string(response.status_code); - try { - auto error_json = JsonValue::parse(response.body); - if (error_json.contains("error") && - error_json["error"].contains("message")) { - error_msg = error_json["error"]["message"].getString(); - } - } catch (...) { - error_msg += ": " + response.body; - } - - int error_code = LLMError::UNKNOWN; - if (response.status_code == 401) { - error_code = LLMError::INVALID_API_KEY; - } else if (response.status_code == 429) { - error_code = LLMError::RATE_LIMITED; - } else if (response.status_code >= 500) { - error_code = LLMError::SERVICE_UNAVAILABLE; - } - - callback(Result(Error(error_code, error_msg))); - return; - } - - try { - auto response_json = JsonValue::parse(response.body); - auto parsed = parseResponse(response_json); - callback(std::move(parsed)); - } catch (const std::exception& e) { - callback(Result( - Error(LLMError::PARSE_ERROR, - std::string("Failed to parse response: ") + e.what()))); - } - }); -} - -void AnthropicProvider::chatStream(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - StreamCallback on_chunk, - ChatCallback on_complete) { - // Fall back to non-streaming for now - chat(messages, tools, config, dispatcher, std::move(on_complete)); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// REQUEST/RESPONSE BUILDING -// ═══════════════════════════════════════════════════════════════════════════ - -JsonValue AnthropicProvider::buildRequest(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - bool stream) const { - JsonValue request = JsonValue::object(); - - // Model - request["model"] = config.model; - - // Convert messages (extract system separately) - auto [system_prompt, anthropic_messages] = - messagesToAnthropicFormat(messages); - - if (!system_prompt.empty()) { - request["system"] = system_prompt; - } - request["messages"] = anthropic_messages; - - // Max tokens (required for Anthropic) - request["max_tokens"] = config.max_tokens.value_or(4096); - - // Tools (if any) - if (!tools.empty()) { - JsonValue tool_array = JsonValue::array(); - for (const auto& tool : tools) { - tool_array.push_back(toolToJson(tool)); - } - request["tools"] = tool_array; - } - - // Optional parameters - if (config.temperature.has_value()) { - request["temperature"] = *config.temperature; - } - if (config.top_p.has_value()) { - request["top_p"] = *config.top_p; - } - if (config.stop.has_value() && !config.stop->empty()) { - JsonValue stop_array = JsonValue::array(); - for (const auto& s : *config.stop) { - stop_array.push_back(s); - } - request["stop_sequences"] = stop_array; - } - - if (stream) { - request["stream"] = true; - } - - return request; -} - -std::pair AnthropicProvider::messagesToAnthropicFormat( - const std::vector& messages) const { - std::string system_prompt; - JsonValue anthropic_messages = JsonValue::array(); - - for (const auto& msg : messages) { - if (msg.role == Role::SYSTEM) { - // Anthropic has separate system field - if (!system_prompt.empty()) { - system_prompt += "\n\n"; - } - system_prompt += msg.content; - continue; - } - - JsonValue json_msg = JsonValue::object(); - - if (msg.role == Role::USER) { - json_msg["role"] = "user"; - - // Check if this is a tool result - if (msg.tool_call_id.has_value()) { - // Tool result format for Anthropic - JsonValue content = JsonValue::array(); - JsonValue tool_result = JsonValue::object(); - tool_result["type"] = "tool_result"; - tool_result["tool_use_id"] = *msg.tool_call_id; - tool_result["content"] = msg.content; - content.push_back(tool_result); - json_msg["content"] = content; - } else { - json_msg["content"] = msg.content; - } - - } else if (msg.role == Role::TOOL) { - // Tool results in Anthropic go in a user message - json_msg["role"] = "user"; - JsonValue content = JsonValue::array(); - JsonValue tool_result = JsonValue::object(); - tool_result["type"] = "tool_result"; - if (msg.tool_call_id.has_value()) { - tool_result["tool_use_id"] = *msg.tool_call_id; - } - tool_result["content"] = msg.content; - content.push_back(tool_result); - json_msg["content"] = content; - - } else if (msg.role == Role::ASSISTANT) { - json_msg["role"] = "assistant"; - - if (msg.hasToolCalls()) { - // Assistant message with tool use - JsonValue content = JsonValue::array(); - - // Add text content if present - if (!msg.content.empty()) { - JsonValue text_block = JsonValue::object(); - text_block["type"] = "text"; - text_block["text"] = msg.content; - content.push_back(text_block); - } - - // Add tool use blocks - for (const auto& tc : *msg.tool_calls) { - JsonValue tool_use = JsonValue::object(); - tool_use["type"] = "tool_use"; - tool_use["id"] = tc.id; - tool_use["name"] = tc.name; - tool_use["input"] = tc.arguments; - content.push_back(tool_use); - } - - json_msg["content"] = content; - } else { - json_msg["content"] = msg.content; - } - } - - anthropic_messages.push_back(json_msg); - } - - return {system_prompt, anthropic_messages}; -} - -Result AnthropicProvider::parseResponse( - const JsonValue& response) const { - LLMResponse result; - - try { - // Parse stop reason - if (response.contains("stop_reason") && !response["stop_reason"].isNull()) { - std::string stop_reason = response["stop_reason"].getString(); - // Map Anthropic stop reasons to our format - if (stop_reason == "end_turn") { - result.finish_reason = "stop"; - } else if (stop_reason == "tool_use") { - result.finish_reason = "tool_calls"; - } else if (stop_reason == "max_tokens") { - result.finish_reason = "length"; - } else { - result.finish_reason = stop_reason; - } - } - - result.message.role = Role::ASSISTANT; - - // Parse content array - if (response.contains("content") && response["content"].isArray()) { - std::string text_content; - std::vector tool_calls; - - const auto& content_array = response["content"]; - for (size_t i = 0; i < content_array.size(); ++i) { - const auto& block = content_array[i]; - std::string block_type = - block.contains("type") ? block["type"].getString() : ""; - - if (block_type == "text") { - if (!text_content.empty()) { - text_content += "\n"; - } - text_content += block["text"].getString(); - - } else if (block_type == "tool_use") { - ToolCall tc; - tc.id = block["id"].getString(); - tc.name = block["name"].getString(); - tc.arguments = block["input"]; - tool_calls.push_back(std::move(tc)); - } - } - - result.message.content = text_content; - if (!tool_calls.empty()) { - result.message.tool_calls = std::move(tool_calls); - } - } - - // Parse usage - if (response.contains("usage")) { - const auto& usage = response["usage"]; - Usage u; - u.prompt_tokens = - usage.contains("input_tokens") ? usage["input_tokens"].getInt() : 0; - u.completion_tokens = - usage.contains("output_tokens") ? usage["output_tokens"].getInt() : 0; - u.total_tokens = u.prompt_tokens + u.completion_tokens; - result.usage = u; - } - - return Result(std::move(result)); - - } catch (const std::exception& e) { - return Result( - Error(LLMError::PARSE_ERROR, std::string("Parse error: ") + e.what())); - } -} - -JsonValue AnthropicProvider::toolToJson(const ToolSpec& tool) const { - JsonValue json = JsonValue::object(); - json["name"] = tool.name; - json["description"] = tool.description; - - // Anthropic expects input_schema with a type field - JsonValue input_schema = tool.parameters; - - // Ensure the schema has a type field (Anthropic requires this) - if (!input_schema.contains("type")) { - // If no type is specified, assume it's an object type - input_schema["type"] = "object"; - } - - json["input_schema"] = input_schema; - - return json; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// MODEL SUPPORT -// ═══════════════════════════════════════════════════════════════════════════ - -bool AnthropicProvider::isModelSupported(const std::string& model) const { - // Accept any model - Anthropic will validate - return !model.empty(); -} - -std::vector AnthropicProvider::supportedModels() const { - return {"claude-3-5-sonnet-latest", "claude-3-5-sonnet-20241022", - "claude-3-5-haiku-latest", "claude-3-5-haiku-20241022", - "claude-3-opus-20240229", "claude-3-sonnet-20240229", - "claude-3-haiku-20240307", "claude-opus-4-5-20251101", - "claude-sonnet-4-20250514"}; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// CONFIGURATION -// ═══════════════════════════════════════════════════════════════════════════ - -std::string AnthropicProvider::endpoint() const { - return impl_->messagesEndpoint(); -} - -bool AnthropicProvider::isConfigured() const { - return !impl_->config.api_key.empty(); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// FACTORY FUNCTION -// ═══════════════════════════════════════════════════════════════════════════ - -LLMProviderPtr createAnthropicProvider(const std::string& api_key, - const std::string& base_url) { - if (base_url.empty()) { - return AnthropicProvider::create(api_key); - } - return AnthropicProvider::create(api_key, base_url); -} - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/llm/llm_factory.cc b/third_party/gopher-orch/src/gopher/orch/llm/llm_factory.cc deleted file mode 100644 index 2eee8c39..00000000 --- a/third_party/gopher-orch/src/gopher/orch/llm/llm_factory.cc +++ /dev/null @@ -1,60 +0,0 @@ -// LLM Provider Factory Implementation - -#include "gopher/orch/llm/anthropic_provider.h" -#include "gopher/orch/llm/llm_provider.h" -#include "gopher/orch/llm/openai_provider.h" - -namespace gopher { -namespace orch { -namespace llm { - -LLMProviderPtr createProvider(const ProviderConfig& config) { - switch (config.type) { - case ProviderType::OPENAI: { - OpenAIConfig openai_config(config.api_key); - if (!config.base_url.empty()) { - openai_config.withBaseUrl(config.base_url); - } - return OpenAIProvider::create(openai_config); - } - - case ProviderType::ANTHROPIC: { - AnthropicConfig anthropic_config(config.api_key); - if (!config.base_url.empty()) { - anthropic_config.withBaseUrl(config.base_url); - } - return AnthropicProvider::create(anthropic_config); - } - - case ProviderType::OLLAMA: { - // Ollama uses OpenAI-compatible API - OpenAIConfig ollama_config(""); - ollama_config.withBaseUrl(config.base_url.empty() - ? "http://localhost:11434/v1" - : config.base_url); - return OpenAIProvider::create(ollama_config); - } - - case ProviderType::CUSTOM: { - // For custom providers, use OpenAI-compatible API by default - OpenAIConfig custom_config(config.api_key); - if (!config.base_url.empty()) { - custom_config.withBaseUrl(config.base_url); - } - return OpenAIProvider::create(custom_config); - } - - default: - return nullptr; - } -} - -LLMProviderPtr createOllamaProvider(const std::string& base_url) { - ProviderConfig config(ProviderType::OLLAMA); - config.base_url = base_url.empty() ? "http://localhost:11434/v1" : base_url; - return createProvider(config); -} - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/llm/llm_runnable.cc b/third_party/gopher-orch/src/gopher/orch/llm/llm_runnable.cc deleted file mode 100644 index cfdcf52e..00000000 --- a/third_party/gopher-orch/src/gopher/orch/llm/llm_runnable.cc +++ /dev/null @@ -1,248 +0,0 @@ -// LLMRunnable Implementation - -#include "gopher/orch/llm/llm_runnable.h" - -namespace gopher { -namespace orch { -namespace llm { - -// ============================================================================= -// Factory -// ============================================================================= - -LLMRunnable::Ptr LLMRunnable::create(LLMProviderPtr provider, - const LLMConfig& config) { - return Ptr(new LLMRunnable(std::move(provider), config)); -} - -LLMRunnable::LLMRunnable(LLMProviderPtr provider, const LLMConfig& config) - : provider_(std::move(provider)), default_config_(config) {} - -// ============================================================================= -// Runnable Interface -// ============================================================================= - -std::string LLMRunnable::name() const { - if (provider_) { - return "LLMRunnable(" + provider_->name() + ")"; - } - return "LLMRunnable"; -} - -void LLMRunnable::invoke(const JsonValue& input, - const RunnableConfig& /* config */, - Dispatcher& dispatcher, - Callback callback) { - // Validate provider - if (!provider_) { - postError(dispatcher, std::move(callback), LLMError::UNKNOWN, - "No LLM provider configured"); - return; - } - - // Parse input - ParsedInput parsed = parseInput(input); - - // Validate messages - if (parsed.messages.empty()) { - postError(dispatcher, std::move(callback), - LLMError::INVALID_MODEL, "No messages provided"); - return; - } - - // Call the LLM provider - provider_->chat( - parsed.messages, parsed.tools, parsed.config, dispatcher, - [callback = std::move(callback)](Result result) mutable { - if (mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - } else { - JsonValue output = responseToJson(mcp::get(result)); - callback(Result(std::move(output))); - } - }); -} - -// ============================================================================= -// Input Parsing -// ============================================================================= - -LLMRunnable::ParsedInput LLMRunnable::parseInput(const JsonValue& input) const { - ParsedInput result; - result.config = default_config_; - - // Handle string input as simple user message - if (input.isString()) { - result.messages.push_back(Message::user(input.getString())); - return result; - } - - // Handle object input - if (!input.isObject()) { - return result; - } - - // Parse messages array - if (input.contains("messages") && input["messages"].isArray()) { - const auto& messages_array = input["messages"]; - for (size_t i = 0; i < messages_array.size(); ++i) { - result.messages.push_back(parseMessage(messages_array[i])); - } - } - - // Parse tools array - if (input.contains("tools") && input["tools"].isArray()) { - const auto& tools_array = input["tools"]; - for (size_t i = 0; i < tools_array.size(); ++i) { - result.tools.push_back(parseToolSpec(tools_array[i])); - } - } - - // Parse config overrides - if (input.contains("config") && input["config"].isObject()) { - const auto& config_obj = input["config"]; - - if (config_obj.contains("model") && config_obj["model"].isString()) { - result.config.model = config_obj["model"].getString(); - } - if (config_obj.contains("temperature") && - config_obj["temperature"].isNumber()) { - result.config.temperature = config_obj["temperature"].getFloat(); - } - if (config_obj.contains("max_tokens") && - config_obj["max_tokens"].isNumber()) { - result.config.max_tokens = config_obj["max_tokens"].getInt(); - } - if (config_obj.contains("top_p") && config_obj["top_p"].isNumber()) { - result.config.top_p = config_obj["top_p"].getFloat(); - } - if (config_obj.contains("seed") && config_obj["seed"].isNumber()) { - result.config.seed = config_obj["seed"].getInt(); - } - } - - return result; -} - -Message LLMRunnable::parseMessage(const JsonValue& json) { - if (!json.isObject()) { - return Message::user(""); - } - - Role role = Role::USER; - if (json.contains("role") && json["role"].isString()) { - role = parseRole(json["role"].getString()); - } - - std::string content; - if (json.contains("content") && json["content"].isString()) { - content = json["content"].getString(); - } - - Message msg(role, content); - - // Parse tool_call_id for tool messages - if (json.contains("tool_call_id") && json["tool_call_id"].isString()) { - msg.tool_call_id = json["tool_call_id"].getString(); - } - - // Parse tool_calls for assistant messages - if (json.contains("tool_calls") && json["tool_calls"].isArray()) { - std::vector calls; - const auto& calls_array = json["tool_calls"]; - for (size_t i = 0; i < calls_array.size(); ++i) { - const auto& call_obj = calls_array[i]; - if (call_obj.isObject()) { - ToolCall call; - if (call_obj.contains("id") && call_obj["id"].isString()) { - call.id = call_obj["id"].getString(); - } - if (call_obj.contains("name") && call_obj["name"].isString()) { - call.name = call_obj["name"].getString(); - } - if (call_obj.contains("arguments")) { - call.arguments = call_obj["arguments"]; - } - calls.push_back(std::move(call)); - } - } - if (!calls.empty()) { - msg.tool_calls = std::move(calls); - } - } - - return msg; -} - -ToolSpec LLMRunnable::parseToolSpec(const JsonValue& json) { - ToolSpec spec; - if (!json.isObject()) { - return spec; - } - - if (json.contains("name") && json["name"].isString()) { - spec.name = json["name"].getString(); - } - if (json.contains("description") && json["description"].isString()) { - spec.description = json["description"].getString(); - } - if (json.contains("parameters")) { - spec.parameters = json["parameters"]; - } - - return spec; -} - -// ============================================================================= -// Output Conversion -// ============================================================================= - -JsonValue LLMRunnable::responseToJson(const LLMResponse& response) { - JsonValue output = JsonValue::object(); - - // Convert message - output["message"] = messageToJson(response.message); - - // Add finish_reason - output["finish_reason"] = response.finish_reason; - - // Add usage if present - if (response.usage.has_value()) { - JsonValue usage = JsonValue::object(); - usage["prompt_tokens"] = response.usage->prompt_tokens; - usage["completion_tokens"] = response.usage->completion_tokens; - usage["total_tokens"] = response.usage->total_tokens; - output["usage"] = usage; - } - - return output; -} - -JsonValue LLMRunnable::messageToJson(const Message& message) { - JsonValue json = JsonValue::object(); - - json["role"] = roleToString(message.role); - json["content"] = message.content; - - if (message.tool_call_id.has_value()) { - json["tool_call_id"] = *message.tool_call_id; - } - - if (message.tool_calls.has_value() && !message.tool_calls->empty()) { - JsonValue calls_array = JsonValue::array(); - for (const auto& call : *message.tool_calls) { - JsonValue call_obj = JsonValue::object(); - call_obj["id"] = call.id; - call_obj["name"] = call.name; - call_obj["arguments"] = call.arguments; - calls_array.push_back(call_obj); - } - json["tool_calls"] = calls_array; - } - - return json; -} - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/llm/openai_provider.cc b/third_party/gopher-orch/src/gopher/orch/llm/openai_provider.cc deleted file mode 100644 index 894ba49b..00000000 --- a/third_party/gopher-orch/src/gopher/orch/llm/openai_provider.cc +++ /dev/null @@ -1,412 +0,0 @@ -// OpenAI Provider Implementation - -#include "gopher/orch/llm/openai_provider.h" - -#include -#include - -#include "gopher/orch/server/rest_server.h" - -namespace gopher { -namespace orch { -namespace llm { - -using namespace gopher::orch::core; -using namespace gopher::orch::server; - -// ═══════════════════════════════════════════════════════════════════════════ -// IMPLEMENTATION -// ═══════════════════════════════════════════════════════════════════════════ - -class OpenAIProvider::Impl { - public: - OpenAIConfig config; - HttpClientPtr http_client; - mutable std::mutex mutex; - - explicit Impl(const OpenAIConfig& cfg) : config(cfg) { - // Use CurlHttpClient for real HTTP requests - http_client = server::createCurlHttpClient(); - } - - std::string chatEndpoint() const { - if (config.is_azure) { - return config.base_url + "/openai/deployments/" + - config.azure_deployment + - "/chat/completions?api-version=" + config.azure_api_version; - } - return config.base_url + "/chat/completions"; - } - - std::map headers() const { - std::map hdrs; - hdrs["Content-Type"] = "application/json"; - - if (config.is_azure) { - hdrs["api-key"] = config.api_key; - } else { - hdrs["Authorization"] = "Bearer " + config.api_key; - if (!config.organization.empty()) { - hdrs["OpenAI-Organization"] = config.organization; - } - } - - return hdrs; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// FACTORY METHODS -// ═══════════════════════════════════════════════════════════════════════════ - -OpenAIProvider::Ptr OpenAIProvider::create(const std::string& api_key) { - return create(OpenAIConfig(api_key)); -} - -OpenAIProvider::Ptr OpenAIProvider::create(const std::string& api_key, - const std::string& base_url) { - OpenAIConfig config(api_key); - if (!base_url.empty()) { - config.withBaseUrl(base_url); - } - return create(config); -} - -OpenAIProvider::Ptr OpenAIProvider::create(const OpenAIConfig& config) { - return Ptr(new OpenAIProvider(config)); -} - -OpenAIProvider::OpenAIProvider(const OpenAIConfig& config) - : impl_(std::make_unique(config)) {} - -OpenAIProvider::~OpenAIProvider() = default; - -// ═══════════════════════════════════════════════════════════════════════════ -// CHAT COMPLETION -// ═══════════════════════════════════════════════════════════════════════════ - -void OpenAIProvider::chat(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) { - // Build request - auto request = buildRequest(messages, tools, config, false); - auto request_body = request.toString(); - - auto url = impl_->chatEndpoint(); - auto headers = impl_->headers(); - - // Make HTTP request - impl_->http_client->request( - HttpMethod::POST, url, headers, request_body, dispatcher, - [this, callback = std::move(callback)](Result result) { - if (!mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - return; - } - - auto& response = mcp::get(result); - if (!response.isSuccess()) { - // Parse error response - std::string error_msg = - "HTTP " + std::to_string(response.status_code); - try { - auto error_json = JsonValue::parse(response.body); - if (error_json.contains("error") && - error_json["error"].contains("message")) { - error_msg = error_json["error"]["message"].getString(); - } - } catch (...) { - error_msg += ": " + response.body; - } - - int error_code = LLMError::UNKNOWN; - if (response.status_code == 401) { - error_code = LLMError::INVALID_API_KEY; - } else if (response.status_code == 429) { - error_code = LLMError::RATE_LIMITED; - } else if (response.status_code >= 500) { - error_code = LLMError::SERVICE_UNAVAILABLE; - } - - callback(Result(Error(error_code, error_msg))); - return; - } - - // Parse response - try { - auto response_json = JsonValue::parse(response.body); - auto parsed = parseResponse(response_json); - callback(std::move(parsed)); - } catch (const std::exception& e) { - callback(Result( - Error(LLMError::PARSE_ERROR, - std::string("Failed to parse response: ") + e.what()))); - } - }); -} - -void OpenAIProvider::chatStream(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - StreamCallback on_chunk, - ChatCallback on_complete) { - // For now, fall back to non-streaming - // Full streaming implementation would require SSE parsing - chat(messages, tools, config, dispatcher, std::move(on_complete)); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// REQUEST/RESPONSE BUILDING -// ═══════════════════════════════════════════════════════════════════════════ - -JsonValue OpenAIProvider::buildRequest(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - bool stream) const { - JsonValue request = JsonValue::object(); - - // Model - request["model"] = config.model; - - // Messages - JsonValue msgs = JsonValue::array(); - for (const auto& msg : messages) { - msgs.push_back(messageToJson(msg)); - } - request["messages"] = msgs; - - // Tools (if any) - if (!tools.empty()) { - JsonValue tool_array = JsonValue::array(); - for (const auto& tool : tools) { - tool_array.push_back(toolToJson(tool)); - } - request["tools"] = tool_array; - } - - // Optional parameters - if (config.temperature.has_value()) { - request["temperature"] = *config.temperature; - } - if (config.max_tokens.has_value()) { - request["max_tokens"] = *config.max_tokens; - } - if (config.top_p.has_value()) { - request["top_p"] = *config.top_p; - } - if (config.seed.has_value()) { - request["seed"] = *config.seed; - } - if (config.stop.has_value() && !config.stop->empty()) { - JsonValue stop_array = JsonValue::array(); - for (const auto& s : *config.stop) { - stop_array.push_back(s); - } - request["stop"] = stop_array; - } - - if (stream) { - request["stream"] = true; - } - - return request; -} - -Result OpenAIProvider::parseResponse( - const JsonValue& response) const { - LLMResponse result; - - try { - // Get the first choice - if (!response.contains("choices") || response["choices"].empty()) { - return Result( - Error(LLMError::PARSE_ERROR, "No choices in response")); - } - - const auto& choice = response["choices"][0]; - - // Parse finish reason - if (choice.contains("finish_reason") && !choice["finish_reason"].isNull()) { - result.finish_reason = choice["finish_reason"].getString(); - } - - // Parse message - if (choice.contains("message")) { - const auto& msg = choice["message"]; - - // Role - if (msg.contains("role")) { - result.message.role = parseRole(msg["role"].getString()); - } else { - result.message.role = Role::ASSISTANT; - } - - // Content - if (msg.contains("content") && !msg["content"].isNull()) { - result.message.content = msg["content"].getString(); - } - - // Tool calls - if (msg.contains("tool_calls") && !msg["tool_calls"].isNull()) { - std::vector tool_calls; - for (size_t i = 0; i < msg["tool_calls"].size(); ++i) { - const auto& tc = msg["tool_calls"][i]; - ToolCall call; - call.id = tc["id"].getString(); - - if (tc.contains("function")) { - call.name = tc["function"]["name"].getString(); - if (tc["function"].contains("arguments")) { - std::string args_str = tc["function"]["arguments"].getString(); - try { - call.arguments = JsonValue::parse(args_str); - } catch (...) { - // If parsing fails, store as string - call.arguments = args_str; - } - } - } - - tool_calls.push_back(std::move(call)); - } - result.message.tool_calls = std::move(tool_calls); - } - } - - // Parse usage - if (response.contains("usage")) { - const auto& usage = response["usage"]; - Usage u; - u.prompt_tokens = - usage.contains("prompt_tokens") ? usage["prompt_tokens"].getInt() : 0; - u.completion_tokens = usage.contains("completion_tokens") - ? usage["completion_tokens"].getInt() - : 0; - u.total_tokens = - usage.contains("total_tokens") ? usage["total_tokens"].getInt() : 0; - result.usage = u; - } - - return Result(std::move(result)); - - } catch (const std::exception& e) { - return Result( - Error(LLMError::PARSE_ERROR, std::string("Parse error: ") + e.what())); - } -} - -JsonValue OpenAIProvider::messageToJson(const Message& msg) const { - JsonValue json = JsonValue::object(); - - json["role"] = roleToString(msg.role); - - // Handle tool results - if (msg.role == Role::TOOL) { - json["role"] = "tool"; - json["content"] = msg.content; - if (msg.tool_call_id.has_value()) { - json["tool_call_id"] = *msg.tool_call_id; - } - return json; - } - - // Regular message content - if (!msg.content.empty()) { - json["content"] = msg.content; - } - - // Tool calls for assistant messages - if (msg.role == Role::ASSISTANT && msg.hasToolCalls()) { - JsonValue tool_calls = JsonValue::array(); - for (const auto& tc : *msg.tool_calls) { - JsonValue call = JsonValue::object(); - call["id"] = tc.id; - call["type"] = "function"; - - JsonValue func = JsonValue::object(); - func["name"] = tc.name; - func["arguments"] = tc.arguments.toString(); - call["function"] = func; - - tool_calls.push_back(call); - } - json["tool_calls"] = tool_calls; - } - - return json; -} - -JsonValue OpenAIProvider::toolToJson(const ToolSpec& tool) const { - JsonValue json = JsonValue::object(); - json["type"] = "function"; - - JsonValue func = JsonValue::object(); - func["name"] = tool.name; - func["description"] = tool.description; - func["parameters"] = tool.parameters; - - json["function"] = func; - return json; -} - -Result OpenAIProvider::parseStreamChunk( - const std::string& data) const { - // SSE data parsing would go here - // For now, return empty chunk - StreamChunk chunk; - return Result(std::move(chunk)); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// MODEL SUPPORT -// ═══════════════════════════════════════════════════════════════════════════ - -bool OpenAIProvider::isModelSupported(const std::string& model) const { - // Accept any model string - OpenAI will validate - // This allows for new models and custom deployments - return !model.empty(); -} - -std::vector OpenAIProvider::supportedModels() const { - return {"gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", - "gpt-3.5-turbo", "o1", "o1-mini", "o1-preview"}; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// CONFIGURATION -// ═══════════════════════════════════════════════════════════════════════════ - -std::string OpenAIProvider::endpoint() const { return impl_->chatEndpoint(); } - -bool OpenAIProvider::isConfigured() const { - return !impl_->config.api_key.empty(); -} - -std::string OpenAIProvider::organization() const { - std::lock_guard lock(impl_->mutex); - return impl_->config.organization; -} - -void OpenAIProvider::setOrganization(const std::string& org) { - std::lock_guard lock(impl_->mutex); - impl_->config.organization = org; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// FACTORY FUNCTION -// ═══════════════════════════════════════════════════════════════════════════ - -LLMProviderPtr createOpenAIProvider(const std::string& api_key, - const std::string& base_url) { - if (base_url.empty()) { - return OpenAIProvider::create(api_key); - } - return OpenAIProvider::create(api_key, base_url); -} - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/server/curl_http_client.cc b/third_party/gopher-orch/src/gopher/orch/server/curl_http_client.cc deleted file mode 100644 index 40e47f0c..00000000 --- a/third_party/gopher-orch/src/gopher/orch/server/curl_http_client.cc +++ /dev/null @@ -1,300 +0,0 @@ -// Curl-based HTTP Client Implementation - -#include "gopher/orch/server/rest_server.h" -#include -#include -#include -#include - -namespace gopher { -namespace orch { -namespace server { - -class CurlHttpClient : public HttpClient { - public: - CurlHttpClient() { - // Initialize curl globally (thread-safe) - curl_global_init(CURL_GLOBAL_ALL); - } - - ~CurlHttpClient() override { - curl_global_cleanup(); - } - - // Utility function for synchronous JSON GET requests - std::string fetchJson(const std::string& url, - const std::map& headers = {}) { - return performSyncRequest(HttpMethod::GET, url, headers, ""); - } - - void request(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - ResponseCallback callback) override { - - // Perform request in a separate thread to avoid blocking - std::thread([method, url, headers, body, &dispatcher, callback]() { - CURL* curl = curl_easy_init(); - if (!curl) { - dispatcher.post([callback]() { - callback(Result( - Error(OrchError::INTERNAL_ERROR, "Failed to initialize CURL"))); - }); - return; - } - - // Response data - std::string response_body; - std::string response_headers; - - // Set URL - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - - // Set method - switch (method) { - case HttpMethod::GET: - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - break; - case HttpMethod::POST: - curl_easy_setopt(curl, CURLOPT_POST, 1L); - break; - case HttpMethod::PUT: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); - break; - case HttpMethod::DELETE_: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - break; - case HttpMethod::PATCH: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); - break; - case HttpMethod::HEAD: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "HEAD"); - curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); - break; - case HttpMethod::OPTIONS: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); - break; - } - - // Set headers - struct curl_slist* header_list = nullptr; - for (const auto& header : headers) { - std::string header_str = header.first + ": " + header.second; - header_list = curl_slist_append(header_list, header_str.c_str()); - } - if (header_list) { - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); - } - - // Set request body - if (!body.empty()) { - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - } - - // Set write callbacks - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_headers); - - // Set timeout - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - - // Enable following redirects - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - // Perform the request - CURLcode res = curl_easy_perform(curl); - - if (res != CURLE_OK) { - std::string error_msg = "CURL error: "; - error_msg += curl_easy_strerror(res); - curl_easy_cleanup(curl); - if (header_list) { - curl_slist_free_all(header_list); - } - - dispatcher.post([callback, error_msg]() { - callback(Result( - Error(OrchError::INTERNAL_ERROR, error_msg))); - }); - return; - } - - // Get status code - long status_code; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); - - // Parse response headers - std::map response_header_map; - std::istringstream header_stream(response_headers); - std::string line; - while (std::getline(header_stream, line)) { - size_t colon_pos = line.find(':'); - if (colon_pos != std::string::npos) { - std::string key = line.substr(0, colon_pos); - std::string value = line.substr(colon_pos + 1); - // Trim whitespace - while (!key.empty() && std::isspace(key.back())) key.pop_back(); - while (!value.empty() && std::isspace(value.front())) value.erase(0, 1); - if (!key.empty()) { - response_header_map[key] = value; - } - } - } - - // Clean up - curl_easy_cleanup(curl); - if (header_list) { - curl_slist_free_all(header_list); - } - - // Create response - HttpResponse response; - response.status_code = static_cast(status_code); - response.headers = std::move(response_header_map); - response.body = std::move(response_body); - - dispatcher.post([callback, response]() { - callback(core::makeSuccess(response)); - }); - }).detach(); - } - - private: - // Synchronous request helper for fetchJson - std::string performSyncRequest(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body) { - CURL* curl = curl_easy_init(); - if (!curl) { - throw std::runtime_error("Failed to initialize CURL"); - } - - // Response data - std::string response_body; - - // Set URL - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - - // Set method - switch (method) { - case HttpMethod::GET: - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - break; - case HttpMethod::POST: - curl_easy_setopt(curl, CURLOPT_POST, 1L); - break; - case HttpMethod::PUT: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); - break; - case HttpMethod::DELETE_: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - break; - case HttpMethod::PATCH: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); - break; - case HttpMethod::HEAD: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "HEAD"); - curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); - break; - case HttpMethod::OPTIONS: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); - break; - } - - // Set headers - struct curl_slist* header_list = nullptr; - for (const auto& header : headers) { - std::string header_str = header.first + ": " + header.second; - header_list = curl_slist_append(header_list, header_str.c_str()); - } - if (header_list) { - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); - } - - // Set request body - if (!body.empty()) { - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - } - - // Set write callback - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); - - // Set timeout - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - - // Enable following redirects - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - // SSL options - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - - // Perform the request - CURLcode res = curl_easy_perform(curl); - - if (res != CURLE_OK) { - std::string error_msg = "HTTP request failed: "; - error_msg += curl_easy_strerror(res); - curl_easy_cleanup(curl); - if (header_list) { - curl_slist_free_all(header_list); - } - throw std::runtime_error(error_msg); - } - - // Get status code - long status_code; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); - - // Clean up - curl_easy_cleanup(curl); - if (header_list) { - curl_slist_free_all(header_list); - } - - // Check for HTTP error status - if (status_code >= 400) { - throw std::runtime_error("HTTP request failed with status " + std::to_string(status_code)); - } - - return response_body; - } - - static size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp) { - size_t total_size = size * nmemb; - std::string* str = static_cast(userp); - str->append(static_cast(contents), total_size); - return total_size; - } - - static size_t headerCallback(void* contents, size_t size, size_t nmemb, void* userp) { - size_t total_size = size * nmemb; - std::string* str = static_cast(userp); - str->append(static_cast(contents), total_size); - return total_size; - } -}; - -// Factory function to create CurlHttpClient -std::shared_ptr createCurlHttpClient() { - return std::make_shared(); -} - -// Utility function for making synchronous JSON HTTP GET requests -std::string fetchJsonSync(const std::string& url, - const std::map& headers) { - CurlHttpClient client; - return client.fetchJson(url, headers); -} - -} // namespace server -} // namespace orch -} // namespace gopher \ No newline at end of file diff --git a/third_party/gopher-orch/src/gopher/orch/server/gateway_server.cpp b/third_party/gopher-orch/src/gopher/orch/server/gateway_server.cpp deleted file mode 100644 index 54fecce2..00000000 --- a/third_party/gopher-orch/src/gopher/orch/server/gateway_server.cpp +++ /dev/null @@ -1,749 +0,0 @@ -/** - * @file gateway_server.cpp - * @brief Implementation of GatewayServer - MCP server exposing ServerComposite tools - */ - -#include "gopher/orch/server/gateway_server.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/server/mcp_server.h" -#include "mcp/event/libevent_dispatcher.h" -#include "mcp/logging/log_macros.h" -#include "nlohmann/json.hpp" - -// Define log component for this file -#undef GOPHER_LOG_COMPONENT -#define GOPHER_LOG_COMPONENT "Gateway" - -namespace gopher { -namespace orch { -namespace server { - -// Global signal handling for graceful shutdown -namespace { - volatile sig_atomic_t g_shutdown_requested = 0; - volatile sig_atomic_t g_signal_count = 0; - mcp::server::McpServer* g_mcp_server = nullptr; - - void signal_handler(int signal) { - if (signal == SIGINT || signal == SIGTERM) { - g_shutdown_requested = 1; - g_signal_count++; - - // Second Ctrl+C forces immediate exit - if (g_signal_count >= 2) { - const char msg[] = "\nForced exit.\n"; - write(STDERR_FILENO, msg, sizeof(msg) - 1); - _exit(1); - } - - // Try graceful shutdown - if (g_mcp_server) { - g_mcp_server->shutdown(); - } - } - } -} - -// Private constructor for JSON-based creation -GatewayServer::GatewayServer(const GatewayServerConfig& config) - : config_(config) { - // Composite will be created by initFromJson() -} - -GatewayServer::GatewayServer(ServerCompositePtr composite, - const GatewayServerConfig& config) - : composite_(std::move(composite)), config_(config) { - // Configure MCP server - mcp::server::McpServerConfig mcp_config; - mcp_config.server_name = config_.name; - mcp_config.server_version = "1.0.0"; - mcp_config.protocol_version = "2024-11-05"; - - // Transport configuration - mcp_config.supported_transports = {mcp::TransportType::HttpSse}; - - // HTTP/SSE paths - mcp_config.http_rpc_path = config_.http_rpc_path; - mcp_config.http_sse_path = config_.http_sse_path; - mcp_config.http_health_path = config_.http_health_path; - - // Session management - mcp_config.max_sessions = config_.max_sessions; - mcp_config.session_timeout = config_.session_timeout; - - // Create MCP server - mcp_server_ = std::make_unique(mcp_config); -} - -GatewayServer::~GatewayServer() { - // Stop the backend dispatcher thread - if (dispatcher_running_) { - dispatcher_running_ = false; - if (dispatcher_thread_ && dispatcher_thread_->joinable()) { - dispatcher_thread_->join(); - } - } - - if (running_.load()) { - // Force shutdown - if (mcp_server_) { - mcp_server_->shutdown(); - } - running_ = false; - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// SIMPLE API IMPLEMENTATION -// ═══════════════════════════════════════════════════════════════════════════ - -GatewayServer::Ptr GatewayServer::create(const std::string& serverJson, - const GatewayServerConfig& config) { - auto gateway = std::shared_ptr(new GatewayServer(config)); - - if (!gateway->initFromJson(serverJson)) { - // Error message is set in initFromJson - return gateway; // Return with error set - } - - return gateway; -} - -bool GatewayServer::initFromJson(const std::string& serverJson) { - try { - auto json = nlohmann::json::parse(serverJson); - - // Check for API response format (with succeeded, code, message, data) - if (json.contains("succeeded") && !json["succeeded"].get()) { - error_message_ = "Server configuration indicates failure"; - if (json.contains("message")) { - error_message_ = json["message"].get(); - } - return false; - } - - // Extract manifest from data (API response) or root (config file) - nlohmann::json manifest; - if (json.contains("data") && json["data"].is_object()) { - // API response format: { succeeded, code, message, data: { version, metadata, config, servers } } - manifest = json["data"]; - } else if (json.contains("servers")) { - // Direct manifest format: { version, metadata, config, servers } - manifest = json; - } else { - error_message_ = "No 'servers' array found in configuration"; - return false; - } - - // Extract servers array - if (!manifest.contains("servers")) { - error_message_ = "No 'servers' array found in configuration"; - return false; - } - nlohmann::json servers_array = manifest["servers"]; - - if (!servers_array.is_array() || servers_array.empty()) { - error_message_ = "Servers array is empty or invalid"; - return false; - } - - // Parse config object (connectTimeout, requestTimeout, retryPolicy) - // Default values - int connect_timeout_ms = 5000; - int request_timeout_ms = 30000; - - if (manifest.contains("config") && manifest["config"].is_object()) { - auto& config_obj = manifest["config"]; - connect_timeout_ms = config_obj.value("connectTimeout", 5000); - request_timeout_ms = config_obj.value("requestTimeout", 30000); - - // Parse retry policy (for logging/future use) - if (config_obj.contains("retryPolicy") && config_obj["retryPolicy"].is_object()) { - auto& retry = config_obj["retryPolicy"]; - int max_attempts = retry.value("maxAttempts", 5); - int initial_backoff = retry.value("initialBackoff", 1000); - double backoff_multiplier = retry.value("backoffMultiplier", 2.0); - int max_backoff = retry.value("maxBackoff", 30000); - double jitter = retry.value("jitter", 0.2); - - } - } else { - // Legacy format: timeouts at root level - connect_timeout_ms = manifest.value("connectTimeout", 5000); - request_timeout_ms = manifest.value("requestTimeout", 30000); - } - - // Create composite - composite_ = ServerComposite::create("gateway-composite"); - - // Create dispatcher for server connections - owned_dispatcher_ = std::make_unique("gateway_init"); - - // Parse and connect to each server - for (const auto& server_obj : servers_array) { - std::string name = server_obj.value("name", "unnamed"); - std::string server_id = server_obj.value("serverId", ""); - - // New format: url is directly on server object - // Also support legacy format: url in config.url - std::string url; - std::string command; - - if (server_obj.contains("url")) { - // New format: direct url field - url = server_obj.value("url", ""); - } else if (server_obj.contains("config")) { - // Legacy format: nested config object - auto server_config = server_obj["config"]; - url = server_config.value("url", ""); - command = server_config.value("command", ""); - } - - // Auto-detect transport type based on available fields - bool is_http = !url.empty(); - bool is_stdio = !command.empty() || server_obj.value("command", "").length() > 0; - - if (!is_http && !is_stdio) { - std::cerr << "Warning: Server '" << name << "' has no url or command, skipping\n"; - continue; - } - - // Create MCPServerConfig - MCPServerConfig mcp_server_config; - mcp_server_config.name = name; - - // Use per-server timeouts if specified, otherwise fall back to global config - int server_connect_timeout = server_obj.value("connectTimeout", connect_timeout_ms); - int server_request_timeout = server_obj.value("requestTimeout", request_timeout_ms); - - mcp_server_config.connect_timeout = std::chrono::milliseconds(server_connect_timeout); - mcp_server_config.request_timeout = std::chrono::milliseconds(server_request_timeout); - - if (is_http) { - // HTTP/SSE transport (auto-negotiates SSE vs Streamable HTTP) - mcp_server_config.transport_type = MCPServerConfig::TransportType::HTTP_SSE; - mcp_server_config.http_sse_transport.url = url; - } else if (is_stdio) { - // Stdio transport - mcp_server_config.transport_type = MCPServerConfig::TransportType::STDIO; - // Check both direct and legacy nested config - if (server_obj.contains("command")) { - mcp_server_config.stdio_transport.command = server_obj.value("command", ""); - } else if (server_obj.contains("config")) { - auto server_config = server_obj["config"]; - mcp_server_config.stdio_transport.command = server_config.value("command", ""); - if (server_config.contains("args") && server_config["args"].is_array()) { - for (const auto& arg : server_config["args"]) { - mcp_server_config.stdio_transport.args.push_back(arg.get()); - } - } - } - } - - // Create and connect synchronously - std::mutex connect_mutex; - std::condition_variable connect_cv; - bool connect_done = false; - bool connect_success = false; - std::string connect_error; - MCPServerPtr server; - - MCPServer::create(mcp_server_config, *owned_dispatcher_, - [&](Result result) { - std::lock_guard lock(connect_mutex); - if (mcp::holds_alternative(result)) { - server = mcp::get(result); - connect_success = true; - } else { - connect_success = false; - connect_error = mcp::get(result).message; - } - connect_done = true; - connect_cv.notify_one(); - }); - - // Run dispatcher until connect completes - std::thread dispatcher_thread([this, &connect_done]() { - while (!connect_done) { - owned_dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - }); - - { - std::unique_lock lock(connect_mutex); - connect_cv.wait_for(lock, std::chrono::seconds(30), - [&connect_done]() { return connect_done; }); - } - - dispatcher_thread.join(); - - if (!connect_success || !server) { - std::cerr << "Warning: Failed to connect to server '" << name << "': " << connect_error << "\n"; - continue; - } - - // Get tools and add to composite - std::mutex tools_mutex; - std::condition_variable tools_cv; - bool tools_done = false; - std::vector server_tools; - - server->listTools(*owned_dispatcher_, - [&](Result> result) { - std::lock_guard lock(tools_mutex); - if (!isError>(result)) { - server_tools = mcp::get>(result); - } - tools_done = true; - tools_cv.notify_one(); - }); - - std::thread tools_thread([this, &tools_done]() { - while (!tools_done) { - owned_dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - }); - - { - std::unique_lock lock(tools_mutex); - tools_cv.wait_for(lock, std::chrono::seconds(10), - [&tools_done]() { return tools_done; }); - } - - tools_thread.join(); - - // Add server to composite and cache tool info - std::vector tool_names; - for (const auto& tool : server_tools) { - tool_names.push_back(tool.name); - // Cache full tool info for later registration - cached_tool_infos_.push_back(tool); - } - - composite_->addServer(server, tool_names, false); - std::cout << "Connected to '" << name << "' with " << tool_names.size() << " tools\n"; - } - - if (composite_->servers().empty()) { - error_message_ = "No servers connected successfully"; - return false; - } - - // Initialize MCP server configuration - mcp::server::McpServerConfig mcp_config; - mcp_config.server_name = config_.name; - mcp_config.server_version = "1.0.0"; - mcp_config.protocol_version = "2024-11-05"; - mcp_config.supported_transports = {mcp::TransportType::HttpSse}; - mcp_config.http_rpc_path = config_.http_rpc_path; - mcp_config.http_sse_path = config_.http_sse_path; - mcp_config.http_health_path = config_.http_health_path; - mcp_config.max_sessions = config_.max_sessions; - mcp_config.session_timeout = config_.session_timeout; - - mcp_server_ = std::make_unique(mcp_config); - - // Start background thread to keep backend dispatcher running - // This is necessary because the MCP clients need their event loop running - // to process responses from backend servers - dispatcher_running_ = true; - dispatcher_thread_ = std::make_unique([this]() { - GOPHER_LOG_DEBUG("Backend dispatcher thread started"); - while (dispatcher_running_) { - owned_dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - GOPHER_LOG_DEBUG("Backend dispatcher thread stopped"); - }); - - return true; - - } catch (const nlohmann::json::exception& e) { - error_message_ = std::string("JSON parse error: ") + e.what(); - return false; - } catch (const std::exception& e) { - error_message_ = std::string("Error: ") + e.what(); - return false; - } -} - -int GatewayServer::listen(int port) { - if (!composite_ || !mcp_server_) { - std::cerr << "Error: Gateway not properly initialized"; - if (!error_message_.empty()) { - std::cerr << ": " << error_message_; - } - std::cerr << "\n"; - return 1; - } - - // Override port if specified - if (port > 0) { - config_.port = port; - } - - // Set up signal handling - g_mcp_server = mcp_server_.get(); - g_shutdown_requested = 0; - g_signal_count = 0; - std::signal(SIGINT, signal_handler); - std::signal(SIGTERM, signal_handler); - - // Register tools from composite - registerToolsFromComposite(); - - // Register ping handler - mcp_server_->registerRequestHandler( - "ping", - [](const mcp::jsonrpc::Request& request, - mcp::server::SessionContext& session) { - auto pong = mcp::make() - .add("pong", true) - .add("timestamp", static_cast(std::time(nullptr))) - .build(); - return mcp::jsonrpc::Response::success( - request.id, mcp::jsonrpc::ResponseResult(pong)); - }); - - // Start listening (MCP server requires http:// prefix for HTTP transport) - std::string listen_url = getListenUrl(); - std::string listen_address = getListenAddress(); - auto listen_result = mcp_server_->listen(listen_url); - - if (mcp::is_error(listen_result)) { - auto error = mcp::get_error(listen_result); - std::cerr << "Failed to start gateway: " << error->message << "\n"; - g_mcp_server = nullptr; - return 1; - } - - running_ = true; - - std::cout << "\n"; - std::cout << "╔════════════════════════════════════════════════════════════╗\n"; - std::cout << "║ Gateway Server Running ║\n"; - std::cout << "╠════════════════════════════════════════════════════════════╣\n"; - std::cout << "║ Address: " << listen_address; - for (size_t i = listen_address.length(); i < 49; ++i) std::cout << " "; - std::cout << "║\n"; - std::cout << "║ Tools: " << tool_count_.load(); - std::string tools_str = std::to_string(tool_count_.load()); - for (size_t i = tools_str.length(); i < 49; ++i) std::cout << " "; - std::cout << "║\n"; - std::cout << "║ Servers: " << serverCount(); - std::string servers_str = std::to_string(serverCount()); - for (size_t i = servers_str.length(); i < 49; ++i) std::cout << " "; - std::cout << "║\n"; - std::cout << "╠════════════════════════════════════════════════════════════╣\n"; - std::cout << "║ Endpoints: ║\n"; - std::cout << "║ GET /health - Health check ║\n"; - std::cout << "║ GET /info - Server info ║\n"; - std::cout << "║ POST /mcp - JSON-RPC endpoint ║\n"; - std::cout << "║ GET /events - SSE event stream ║\n"; - std::cout << "╠════════════════════════════════════════════════════════════╣\n"; - std::cout << "║ List tools: curl -X POST http://" << listen_address << "/mcp"; - for (size_t i = listen_address.length(); i < 22; ++i) std::cout << " "; - std::cout << "║\n"; - std::cout << "║ -H 'Content-Type: application/json' ║\n"; - std::cout << "║ -d '{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}' ║\n"; - std::cout << "╠════════════════════════════════════════════════════════════╣\n"; - std::cout << "║ Press Ctrl+C to stop ║\n"; - std::cout << "╚════════════════════════════════════════════════════════════╝\n"; - std::cout << "\n"; - - // Run the server (blocks until shutdown) - mcp_server_->run(); - - running_ = false; - g_mcp_server = nullptr; - - std::cout << "\nGateway server stopped.\n"; - return 0; -} - -void GatewayServer::stop() { - shutdown_requested_ = true; - - // Stop the backend dispatcher thread - if (dispatcher_running_) { - dispatcher_running_ = false; - if (dispatcher_thread_ && dispatcher_thread_->joinable()) { - dispatcher_thread_->join(); - } - } - - if (mcp_server_ && running_.load()) { - mcp_server_->shutdown(); - running_ = false; - } -} - -size_t GatewayServer::serverCount() const { - return composite_ ? composite_->servers().size() : 0; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// ADVANCED API IMPLEMENTATION -// ═══════════════════════════════════════════════════════════════════════════ - -void GatewayServer::start(Dispatcher& dispatcher, - std::function callback) { - if (running_.load()) { - dispatcher.post([callback]() { - callback(VoidResult(Error(-1, "Gateway server already running"))); - }); - return; - } - - // Register tools from composite - registerToolsFromComposite(); - - // Register ping handler for client keep-alive - mcp_server_->registerRequestHandler( - "ping", - [](const mcp::jsonrpc::Request& request, - mcp::server::SessionContext& session) { - auto pong = mcp::make() - .add("pong", true) - .add("timestamp", static_cast(std::time(nullptr))) - .build(); - return mcp::jsonrpc::Response::success( - request.id, mcp::jsonrpc::ResponseResult(pong)); - }); - - // Start listening (MCP server requires http:// prefix for HTTP transport) - std::string listen_url = getListenUrl(); - auto listen_result = mcp_server_->listen(listen_url); - - if (mcp::is_error(listen_result)) { - auto error = mcp::get_error(listen_result); - dispatcher.post([callback, error]() { - callback(VoidResult(Error(-1, "Failed to start gateway: " + error->message))); - }); - return; - } - - running_ = true; - - // Start the server in a separate thread - std::thread server_thread([this]() { - mcp_server_->run(); - }); - server_thread.detach(); - - dispatcher.post([callback]() { callback(VoidResult(nullptr)); }); -} - -void GatewayServer::stop(Dispatcher& dispatcher, - std::function callback) { - if (!running_.load()) { - if (callback) { - dispatcher.post(callback); - } - return; - } - - mcp_server_->shutdown(); - running_ = false; - - if (callback) { - dispatcher.post(callback); - } -} - -void GatewayServer::registerToolsFromComposite() { - if (!composite_) { - return; - } - - // Use cached tool infos (with descriptions and schemas) if available, - // otherwise fall back to composite's list (names only) - const auto& tool_infos = cached_tool_infos_.empty() - ? composite_->listToolInfos() - : cached_tool_infos_; - size_t count = 0; - - for (const auto& info : tool_infos) { - // Create MCP Tool from ServerToolInfo - mcp::Tool mcp_tool; - mcp_tool.name = info.name; - - if (!info.description.empty()) { - mcp_tool.description = mcp::make_optional(info.description); - } - - // Copy input schema if available - if (!info.inputSchema.isNull()) { - mcp_tool.inputSchema = mcp::make_optional(info.inputSchema); - } - - // Register tool with handler that routes through composite - // Capture tool name by value for the lambda - std::string tool_name = info.name; - - mcp_server_->registerTool( - mcp_tool, - [this, tool_name](const std::string& name, - const mcp::optional& arguments, - mcp::server::SessionContext& session) -> mcp::CallToolResult { - GOPHER_LOG_DEBUG("Tool handler invoked for: {}", tool_name); - try { - auto result = handleToolCall(tool_name, arguments, session); - GOPHER_LOG_DEBUG("Tool handler completed successfully for: {}", tool_name); - return result; - } catch (const std::exception& e) { - GOPHER_LOG_DEBUG("Exception in tool handler for {}: {}", tool_name, e.what()); - mcp::CallToolResult error_result; - error_result.isError = true; - error_result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent("Error: " + std::string(e.what())))); - return error_result; - } catch (...) { - GOPHER_LOG_DEBUG("Unknown exception in tool handler for: {}", tool_name); - mcp::CallToolResult error_result; - error_result.isError = true; - error_result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent("Error: Unknown exception"))); - return error_result; - } - }); - - ++count; - } - - tool_count_ = count; -} - -mcp::CallToolResult GatewayServer::handleToolCall( - const std::string& tool_name, - const mcp::optional& arguments, - mcp::server::SessionContext& session) { - - GOPHER_LOG_DEBUG("handleToolCall invoked for tool: {}", tool_name); - - mcp::CallToolResult result; - - // Get tool from composite - auto tool = composite_->tool(tool_name); - if (!tool) { - GOPHER_LOG_DEBUG("Tool not found in composite: {}", tool_name); - result.isError = true; - result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent("Tool not found: " + tool_name))); - return result; - } - GOPHER_LOG_DEBUG("Tool found in composite: {}", tool_name); - - // Convert MCP Metadata to JsonValue - JsonValue args; - if (arguments.has_value()) { - // Convert Metadata map to JsonValue object - for (const auto& pair : arguments.value()) { - const std::string& key = pair.first; - const mcp::MetadataValue& value = pair.second; - - // Handle different MetadataValue types - if (mcp::holds_alternative(value)) { - args[key] = mcp::get(value); - } else if (mcp::holds_alternative(value)) { - args[key] = static_cast(mcp::get(value)); - } else if (mcp::holds_alternative(value)) { - args[key] = mcp::get(value); - } else if (mcp::holds_alternative(value)) { - args[key] = mcp::get(value); - } - // Note: nested objects/arrays would need more complex handling - } - } - - // Use the owned dispatcher that was used to connect the backend servers - // This is critical - the backend MCP clients are bound to this dispatcher - if (!owned_dispatcher_) { - result.isError = true; - result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent("Internal error: no dispatcher"))); - return result; - } - - // Execute tool synchronously using mutex and condition variable - // The MCP server expects synchronous handlers, but our composite uses async - std::mutex mtx; - std::condition_variable cv; - bool completed = false; - Result tool_result = Result(Error(-1, "Timeout")); - - RunnableConfig runnable_config; - tool->invoke( - args, runnable_config, *owned_dispatcher_, - [&completed, &tool_result, &cv, &mtx](Result res) { - { - std::lock_guard lock(mtx); - tool_result = std::move(res); - completed = true; - } - cv.notify_all(); - }); - - // Wait for completion with timeout - // The background dispatcher thread will process the response - auto deadline = std::chrono::steady_clock::now() + config_.request_timeout; - - { - std::unique_lock lock(mtx); - while (!completed && !g_shutdown_requested) { - auto status = cv.wait_until(lock, deadline); - if (status == std::cv_status::timeout && !completed) { - result.isError = true; - result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent("Tool execution timeout"))); - return result; - } - } - } - - // Check if we're shutting down - if (g_shutdown_requested && !completed) { - result.isError = true; - result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent("Server shutting down"))); - return result; - } - - // Convert result to CallToolResult - if (core::isError(tool_result)) { - result.isError = true; - auto error = core::getError(tool_result); - result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent("Error: " + error.message))); - } else { - auto& value = mcp::get(tool_result); - - // Convert JsonValue to text representation - std::string text_result; - if (value.isString()) { - text_result = value.getString(); - } else { - text_result = value.toString(); - } - - result.content.push_back( - mcp::ExtendedContentBlock(mcp::TextContent(text_result))); - } - - return result; -} - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/server/mcp_gateway_main.cpp b/third_party/gopher-orch/src/gopher/orch/server/mcp_gateway_main.cpp deleted file mode 100644 index ad05b390..00000000 --- a/third_party/gopher-orch/src/gopher/orch/server/mcp_gateway_main.cpp +++ /dev/null @@ -1,323 +0,0 @@ -/** - * @file mcp_gateway_main.cpp - * @brief Production MCP Gateway Server binary - * - * This is a production-ready gateway server that reads configuration from: - * 1. MCP_GATEWAY_CONFIG environment variable (JSON string) - * 2. MCP_GATEWAY_CONFIG_PATH file path (default: /etc/mcp/gateway-config.json) - * 3. MCP_GATEWAY_CONFIG_URL API endpoint (with optional MCP_GATEWAY_ACCESS_KEY) - * - * Environment Variables: - * MCP_GATEWAY_CONFIG - JSON config string (highest priority) - * MCP_GATEWAY_CONFIG_PATH - Path to config file (default: /etc/mcp/gateway-config.json) - * MCP_GATEWAY_CONFIG_URL - API URL to fetch config from - * MCP_GATEWAY_ACCESS_KEY - Access key for API authentication - * MCP_GATEWAY_PORT - Server port (default: 3003) - * MCP_GATEWAY_HOST - Server host (default: 0.0.0.0) - * MCP_GATEWAY_NAME - Server name (default: mcp-gateway) - * - * Configuration Formats: - * Manifest format (MCP_GATEWAY_CONFIG, MCP_GATEWAY_CONFIG_PATH): - * { - * "version": "2026-01-11", - * "metadata": { - * "accountId": "348716338765762562", - * "gatewayId": "694821867856330753", - * "gatewayName": "mcp-toolkit-01", - * "generatedAt": 1768114552523 - * }, - * "config": { - * "connectTimeout": 5000, - * "requestTimeout": 30000, - * "retryPolicy": { - * "maxAttempts": 5, - * "initialBackoff": 1000, - * "backoffMultiplier": 2.0, - * "maxBackoff": 30000, - * "jitter": 0.2, - * "retryableCodes": [429, 500, 502, 503, 504] - * } - * }, - * "servers": [ - * {"serverId": "...", "version": "2025-01-09", "name": "...", "url": "http://..."} - * ] - * } - * - * API response format (MCP_GATEWAY_CONFIG_URL): - * { - * "succeeded": true, - * "code": 200000000, - * "message": "success", - * "data": { } - * } - * - * Usage: - * # With environment variable config - * export MCP_GATEWAY_CONFIG='{"version":"2026-01-11","metadata":{"gatewayId":"123"},"config":{},"servers":[{"name":"server1","url":"http://localhost:3001/mcp"}]}' - * ./mcp_gateway - * - * # With config file - * export MCP_GATEWAY_CONFIG_PATH=/etc/mcp/my-config.json - * ./mcp_gateway - * - * # With API fetch - * export MCP_GATEWAY_CONFIG_URL=https://api.example.com/v1/mcp-gateway/123/manifest - * export MCP_GATEWAY_ACCESS_KEY=your-access-key - * ./mcp_gateway - */ - -#include -#include -#include -#include -#include - -#include - -#include "gopher/orch/server/gateway_server.h" - -using namespace gopher::orch::server; - -namespace { - -// Default configuration values -constexpr const char* DEFAULT_CONFIG_PATH = "/etc/mcp/gateway-config.json"; -constexpr int DEFAULT_PORT = 3003; -constexpr const char* DEFAULT_HOST = "0.0.0.0"; -constexpr const char* DEFAULT_NAME = "mcp-gateway"; - -// Environment variable names -constexpr const char* ENV_CONFIG = "MCP_GATEWAY_CONFIG"; -constexpr const char* ENV_CONFIG_PATH = "MCP_GATEWAY_CONFIG_PATH"; -constexpr const char* ENV_CONFIG_URL = "MCP_GATEWAY_CONFIG_URL"; -constexpr const char* ENV_ACCESS_KEY = "MCP_GATEWAY_ACCESS_KEY"; -constexpr const char* ENV_PORT = "MCP_GATEWAY_PORT"; -constexpr const char* ENV_HOST = "MCP_GATEWAY_HOST"; -constexpr const char* ENV_NAME = "MCP_GATEWAY_NAME"; - -/** - * Get environment variable with default value - */ -std::string getEnv(const char* name, const std::string& defaultValue = "") { - const char* value = std::getenv(name); - return value ? std::string(value) : defaultValue; -} - -/** - * Get environment variable as integer with default value - */ -int getEnvInt(const char* name, int defaultValue) { - const char* value = std::getenv(name); - if (value) { - try { - return std::stoi(value); - } catch (...) { - std::cerr << "Warning: Invalid integer value for " << name - << ", using default: " << defaultValue << std::endl; - } - } - return defaultValue; -} - -/** - * Check if a file exists - */ -bool fileExists(const std::string& path) { - std::ifstream file(path); - return file.good(); -} - -/** - * Read file contents - */ -std::string readFile(const std::string& path) { - std::ifstream file(path); - if (!file.is_open()) { - throw std::runtime_error("Failed to open file: " + path); - } - std::stringstream buffer; - buffer << file.rdbuf(); - return buffer.str(); -} - -/** - * CURL write callback for API fetch - */ -size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, - std::string* output) { - size_t totalSize = size * nmemb; - output->append(static_cast(contents), totalSize); - return totalSize; -} - -/** - * Fetch config from API URL - */ -std::string fetchConfigFromUrl(const std::string& url, - const std::string& accessKey) { - CURL* curl = curl_easy_init(); - if (!curl) { - throw std::runtime_error("Failed to initialize CURL"); - } - - std::string response; - struct curl_slist* headers = nullptr; - - // Set URL - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - - // Set write callback - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - - // Set headers - headers = curl_slist_append(headers, "Accept: application/json"); - headers = curl_slist_append(headers, "Content-Type: application/json"); - - // Add access key if provided - if (!accessKey.empty()) { - std::string authHeader = "X-Access-Key: " + accessKey; - headers = curl_slist_append(headers, authHeader.c_str()); - } - - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - - // Set timeout - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); - - // Follow redirects - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - // Perform request - CURLcode res = curl_easy_perform(curl); - - // Check HTTP response code - long httpCode = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); - - // Cleanup - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - if (res != CURLE_OK) { - throw std::runtime_error(std::string("CURL request failed: ") + - curl_easy_strerror(res)); - } - - if (httpCode != 200) { - throw std::runtime_error("API request failed with HTTP " + - std::to_string(httpCode)); - } - - return response; -} - -/** - * Load configuration from available sources - * Priority: ENV_CONFIG > ENV_CONFIG_PATH file > ENV_CONFIG_URL - */ -std::string loadConfig() { - // 1. Try environment variable (direct JSON) - std::string configEnv = getEnv(ENV_CONFIG); - if (!configEnv.empty()) { - return configEnv; - } - - // 2. Try config file path - std::string configPath = getEnv(ENV_CONFIG_PATH, DEFAULT_CONFIG_PATH); - if (fileExists(configPath)) { - return readFile(configPath); - } - - // 3. Try API URL - std::string configUrl = getEnv(ENV_CONFIG_URL); - if (!configUrl.empty()) { - std::string accessKey = getEnv(ENV_ACCESS_KEY); - return fetchConfigFromUrl(configUrl, accessKey); - } - - // No config found - throw std::runtime_error( - "No configuration found. Provide one of:\n" - " - " + - std::string(ENV_CONFIG) + - " environment variable (JSON string)\n" - " - " + - std::string(ENV_CONFIG_PATH) + - " file path (default: " + DEFAULT_CONFIG_PATH + - ")\n" - " - " + - std::string(ENV_CONFIG_URL) + " API endpoint URL"); -} - -/** - * Print startup banner - */ -void printBanner(const GatewayServerConfig& config) { - std::cout << std::endl; - std::cout << "╔═══════════════════════════════════════════════════════════╗" - << std::endl; - std::cout << "║ MCP Gateway Server (Production) ║" - << std::endl; - std::cout << "╠═══════════════════════════════════════════════════════════╣" - << std::endl; - std::cout << "║ Name: " << config.name; - for (size_t i = config.name.length(); i < 51; ++i) std::cout << " "; - std::cout << "║" << std::endl; - std::cout << "║ Host: " << config.host; - for (size_t i = config.host.length(); i < 51; ++i) std::cout << " "; - std::cout << "║" << std::endl; - std::cout << "║ Port: " << config.port; - std::string portStr = std::to_string(config.port); - for (size_t i = portStr.length(); i < 51; ++i) std::cout << " "; - std::cout << "║" << std::endl; - std::cout << "╚═══════════════════════════════════════════════════════════╝" - << std::endl; - std::cout << std::endl; -} - -} // namespace - -int main(int argc, char* argv[]) { - // Initialize CURL globally - curl_global_init(CURL_GLOBAL_ALL); - - try { - // Load configuration - std::string serverJson = loadConfig(); - - // Build gateway config from environment - GatewayServerConfig config; - config.name = getEnv(ENV_NAME, DEFAULT_NAME); - config.host = getEnv(ENV_HOST, DEFAULT_HOST); - config.port = getEnvInt(ENV_PORT, DEFAULT_PORT); - - // Print startup banner - printBanner(config); - - // Create gateway from JSON configuration - auto gateway = GatewayServer::create(serverJson, config); - - // Check if creation succeeded - if (!gateway->getError().empty()) { - std::cerr << "Error: " << gateway->getError() << std::endl; - curl_global_cleanup(); - return 1; - } - - std::cout << "Gateway ready: " << gateway->serverCount() << " servers, " - << gateway->toolCount() << " tools" << std::endl; - - // Start listening (blocks until Ctrl+C or SIGTERM) - int result = gateway->listen(config.port); - - curl_global_cleanup(); - return result; - - } catch (const std::exception& e) { - std::cerr << "Fatal error: " << e.what() << std::endl; - curl_global_cleanup(); - return 1; - } -} diff --git a/third_party/gopher-orch/src/gopher/orch/server/mcp_server.cc b/third_party/gopher-orch/src/gopher/orch/server/mcp_server.cc deleted file mode 100644 index 5c0b7938..00000000 --- a/third_party/gopher-orch/src/gopher/orch/server/mcp_server.cc +++ /dev/null @@ -1,602 +0,0 @@ -// MCPServer implementation -// -// Wraps the gopher-mcp client to implement the protocol-agnostic Server -// interface. All callbacks are invoked in dispatcher thread context. - -#include "gopher/orch/server/mcp_server.h" - -#include -#include - -#include "mcp/json/json_serialization.h" -#include "mcp/logging/log_macros.h" - -// Define log component for this file -#undef GOPHER_LOG_COMPONENT -#define GOPHER_LOG_COMPONENT "MCPServer" - -namespace gopher { -namespace orch { -namespace server { - -// Import orch core utilities -using namespace gopher::orch::core; - -namespace { - -// Atomic counter for generating unique IDs -std::atomic g_id_counter{0}; - -// Helper to convert variant content to JsonValue for C++14 -// Instead of std::visit (C++17), we use type checking and dispatching -template -JsonValue contentToJsonSingle(const T& content); - -template <> -JsonValue contentToJsonSingle(const mcp::TextContent& text) { - JsonValue result = JsonValue::object(); - result["type"] = "text"; - result["text"] = text.text; - return result; -} - -template <> -JsonValue contentToJsonSingle(const mcp::ImageContent& image) { - JsonValue result = JsonValue::object(); - result["type"] = "image"; - result["data"] = image.data; - result["mimeType"] = image.mimeType; - return result; -} - -template <> -JsonValue contentToJsonSingle(const mcp::AudioContent& audio) { - JsonValue result = JsonValue::object(); - result["type"] = "audio"; - result["data"] = audio.data; - result["mimeType"] = audio.mimeType; - return result; -} - -template <> -JsonValue contentToJsonSingle(const mcp::ResourceLink& link) { - JsonValue result = JsonValue::object(); - result["type"] = "resource_link"; - // ResourceLink inherits from Resource, so uri and name are direct members - result["uri"] = link.uri; - if (!link.name.empty()) { - result["name"] = link.name; - } - return result; -} - -template <> -JsonValue contentToJsonSingle(const mcp::EmbeddedResource& embedded) { - JsonValue result = JsonValue::object(); - result["type"] = "embedded_resource"; - // EmbeddedResource has a nested resource member - result["uri"] = embedded.resource.uri; - if (!embedded.resource.name.empty()) { - result["name"] = embedded.resource.name; - } - return result; -} - -// Convert a single ExtendedContentBlock to JsonValue -// Uses mcp::holds_alternative and mcp::get for C++14 variant access -JsonValue extendedContentBlockToJson(const mcp::ExtendedContentBlock& block) { - if (mcp::holds_alternative(block)) { - return contentToJsonSingle(mcp::get(block)); - } else if (mcp::holds_alternative(block)) { - return contentToJsonSingle(mcp::get(block)); - } else if (mcp::holds_alternative(block)) { - return contentToJsonSingle(mcp::get(block)); - } else if (mcp::holds_alternative(block)) { - return contentToJsonSingle(mcp::get(block)); - } else if (mcp::holds_alternative(block)) { - return contentToJsonSingle(mcp::get(block)); - } - return JsonValue::null(); -} - -} // namespace - -// Generate unique ID -std::string MCPServer::generateId() { - std::ostringstream oss; - oss << "mcp-server-" << ++g_id_counter; - return oss.str(); -} - -// Constructor -MCPServer::MCPServer(const MCPServerConfig& config) - : id_(generateId()), config_(config) {} - -// Destructor -MCPServer::~MCPServer() { - // Client cleanup is handled by unique_ptr -} - -// Factory method -void MCPServer::create(const MCPServerConfig& config, - Dispatcher& dispatcher, - std::function)> callback, - bool auto_connect) { - // Create the server instance - // We need to use a raw ptr temporarily then wrap in shared_ptr - MCPServer* raw_server = new MCPServer(config); - auto server = std::shared_ptr(raw_server); - - if (auto_connect) { - // Start connection process - server->initialize(dispatcher, std::move(callback)); - } else { - // Return immediately, user must call connect() - MCPServerPtr server_copy = server; - dispatcher.post( - [callback, server_copy]() { callback(makeSuccess(server_copy)); }); - } -} - -// Initialize connection -void MCPServer::initialize(Dispatcher& dispatcher, - std::function)> callback) { - state_ = ConnectionState::CONNECTING; - - // Create MCP client configuration - mcp::client::McpClientConfig client_config; - client_config.client_name = config_.client_name; - client_config.client_version = config_.client_version; - client_config.request_timeout = config_.request_timeout; - client_config.protocol_initialization_timeout = config_.connect_timeout; - client_config.max_retries = config_.max_connect_retries; - client_config.initial_retry_delay = config_.retry_delay; - - // Set transport type - switch (config_.transport_type) { - case MCPServerConfig::TransportType::STDIO: - client_config.preferred_transport = mcp::TransportType::Stdio; - break; - case MCPServerConfig::TransportType::HTTP_SSE: - client_config.preferred_transport = mcp::TransportType::HttpSse; - break; - case MCPServerConfig::TransportType::WEBSOCKET: - client_config.preferred_transport = mcp::TransportType::WebSocket; - break; - } - - // Create the MCP client - client_ = std::make_unique(client_config); - - // Build connection URI based on transport type - std::string uri; - switch (config_.transport_type) { - case MCPServerConfig::TransportType::STDIO: { - // For stdio, we need to construct the command URI - // Format: stdio://?arg1&arg2... - std::ostringstream oss; - oss << "stdio://" << config_.stdio_transport.command; - if (!config_.stdio_transport.args.empty()) { - oss << "?"; - for (size_t i = 0; i < config_.stdio_transport.args.size(); ++i) { - if (i > 0) - oss << "&"; - oss << config_.stdio_transport.args[i]; - } - } - uri = oss.str(); - break; - } - case MCPServerConfig::TransportType::HTTP_SSE: - uri = config_.http_sse_transport.url; - break; - case MCPServerConfig::TransportType::WEBSOCKET: - uri = config_.websocket_transport.url; - break; - } - - // Connect to the server - mcp::VoidResult connect_result = client_->connect(uri); - if (mcp::holds_alternative(connect_result)) { - state_ = ConnectionState::FAILED; - const mcp::Error& err = mcp::get(connect_result); - callback(makeOrchError( - OrchError::CONNECTION_FAILED, - "Failed to connect to MCP server: " + err.message)); - return; - } - - // Wait for connection to be fully established before protocol initialization - // The connect() call above only initiates async connection - we need to wait - // for the connection event to be processed and connected_ flag to be set - MCPServer* this_ptr = this; - auto self = std::shared_ptr(std::static_pointer_cast( - this_ptr->Server::shared_from_this())); - - // Poll for connection readiness with timeout - // Use the connect_timeout from config, or default to 15 seconds (SSL handshake can take several seconds) - auto start_time = std::make_shared(std::chrono::steady_clock::now()); - auto timeout = config_.connect_timeout.count() > 0 ? config_.connect_timeout : std::chrono::seconds(15); - - // CRITICAL FIX: Capture dispatcher by pointer instead of reference to avoid - // dangling reference when lambdas are invoked asynchronously after this - // function returns. - Dispatcher* dispatcher_ptr = &dispatcher; - - auto waitForConnection = std::make_shared>(); - *waitForConnection = [self, callback, dispatcher_ptr, start_time, timeout, waitForConnection]() { - if (self->client_->isConnected()) { - // Connection is ready - now initialize protocol - auto init_future_ptr = std::make_shared>( - self->client_->initializeProtocol()); - - // CRITICAL FIX: Use non-blocking polling instead of blocking get(). - // Blocking get() inside a posted callback prevents the event loop from - // running, which means timeouts can't trigger and the loop appears hung. - auto waitForInit = std::make_shared>(); - *waitForInit = [self, callback, init_future_ptr, dispatcher_ptr, start_time, timeout, waitForInit]() { - // Check timeout first - auto elapsed = std::chrono::steady_clock::now() - *start_time; - if (elapsed > timeout) { - self->state_ = ConnectionState::FAILED; - auto timeout_secs = std::chrono::duration_cast(timeout).count(); - callback(makeOrchError( - OrchError::CONNECTION_FAILED, - "Init timeout - protocol initialization took longer than " + - std::to_string(timeout_secs) + " seconds")); - return; - } - - // Non-blocking check if future is ready - if (init_future_ptr->wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) { - try { - mcp::InitializeResult init_result = init_future_ptr->get(); - self->onInitialized(*dispatcher_ptr, init_result, callback, start_time, timeout); - } catch (const std::exception& e) { - self->state_ = ConnectionState::FAILED; - callback(makeOrchError( - OrchError::CONNECTION_FAILED, - std::string("Failed to initialize MCP protocol: ") + e.what())); - } - } else { - // Not ready yet - schedule another check in 10ms using timer - // CRITICAL: Create a shared timer holder that captures itself in callback - // to keep the timer alive until it fires - struct TimerHolder { - mcp::event::TimerPtr timer; - }; - auto holder = std::make_shared(); - holder->timer = dispatcher_ptr->createTimer([holder, waitForInit]() { - (void)holder; // Keep holder alive until callback fires - (*waitForInit)(); - }); - holder->timer->enableTimer(std::chrono::milliseconds(10)); - } - }; - (*waitForInit)(); - } else { - // Check timeout - auto elapsed = std::chrono::steady_clock::now() - *start_time; - if (elapsed > timeout) { - self->state_ = ConnectionState::FAILED; - auto timeout_secs = std::chrono::duration_cast(timeout).count(); - callback(makeOrchError( - OrchError::CONNECTION_FAILED, - "Connection timeout - failed to establish connection within " + - std::to_string(timeout_secs) + " seconds")); - return; - } - - // Not connected yet - retry in 10ms using timer (non-blocking) - // CRITICAL: Create a shared timer holder that captures itself in callback - struct TimerHolder { - mcp::event::TimerPtr timer; - }; - auto holder = std::make_shared(); - holder->timer = dispatcher_ptr->createTimer([holder, waitForConnection]() { - (void)holder; // Keep holder alive until callback fires - (*waitForConnection)(); - }); - holder->timer->enableTimer(std::chrono::milliseconds(10)); - } - }; - - // Start the connection polling - dispatcher_ptr->post(*waitForConnection); -} - -// Handle protocol initialization complete -void MCPServer::onInitialized( - Dispatcher& dispatcher, - const mcp::InitializeResult& init_result, - std::function)> callback, - std::shared_ptr start_time, - std::chrono::milliseconds timeout) { - // Store server info and capabilities - if (init_result.serverInfo) { - server_info_ = *init_result.serverInfo; - } - capabilities_ = init_result.capabilities; - - // List available tools - auto self = std::static_pointer_cast(Server::shared_from_this()); - auto tools_future_ptr = - std::make_shared>(client_->listTools()); - - // CRITICAL FIX: Use non-blocking polling instead of blocking get(). - Dispatcher* dispatcher_ptr = &dispatcher; - auto waitForTools = std::make_shared>(); - *waitForTools = [self, callback, tools_future_ptr, dispatcher_ptr, start_time, timeout, waitForTools]() { - // Check timeout first - auto elapsed = std::chrono::steady_clock::now() - *start_time; - if (elapsed > timeout) { - // Tools listing timed out, but connection is still valid - self->state_ = ConnectionState::CONNECTED; - callback(makeSuccess(self)); - return; - } - - // Non-blocking check if future is ready - if (tools_future_ptr->wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) { - try { - mcp::ListToolsResult tools_result = tools_future_ptr->get(); - self->onToolsListed(tools_result); - self->state_ = ConnectionState::CONNECTED; - - // Execute any pending callbacks - for (auto& pending : self->pending_on_connect_) { - pending(); - } - self->pending_on_connect_.clear(); - - callback(makeSuccess(self)); - } catch (const std::exception& e) { - // Tools listing failed, but connection is still valid - self->state_ = ConnectionState::CONNECTED; - callback(makeSuccess(self)); - } - } else { - // Not ready yet - schedule another check in 10ms using timer - // CRITICAL: Create a shared timer holder that captures itself in callback - struct TimerHolder { - mcp::event::TimerPtr timer; - }; - auto holder = std::make_shared(); - holder->timer = dispatcher_ptr->createTimer([holder, waitForTools]() { - (void)holder; // Keep holder alive until callback fires - (*waitForTools)(); - }); - holder->timer->enableTimer(std::chrono::milliseconds(10)); - } - }; - (*waitForTools)(); -} - -// Handle tools listed -void MCPServer::onToolsListed(const mcp::ListToolsResult& tools_result) { - tools_.clear(); - tools_.reserve(tools_result.tools.size()); - - for (const auto& mcp_tool : tools_result.tools) { - tools_.push_back(toServerToolInfo(mcp_tool)); - } -} - -// Convert MCP Tool to ServerToolInfo -ServerToolInfo MCPServer::toServerToolInfo(const mcp::Tool& tool) { - ServerToolInfo info; - info.name = tool.name; - if (tool.description) { - info.description = *tool.description; - } - if (tool.inputSchema) { - // Convert ToolInputSchema to JsonValue - // The inputSchema is already JSON compatible - // info.inputSchema = JsonValue::object(); - info.inputSchema = *(tool.inputSchema); - // TODO: Proper conversion of input schema when needed - } - return info; -} - -// Convert MCP content to JsonValue -JsonValue MCPServer::contentToJson( - const std::vector& content) { - if (content.empty()) { - return JsonValue::null(); - } - - if (content.size() == 1) { - return extendedContentBlockToJson(content[0]); - } - - // Multiple content blocks - return as array - JsonValue result = JsonValue::array(); - for (const auto& block : content) { - result.push_back(extendedContentBlockToJson(block)); - } - return result; -} - -// Connect to the server -void MCPServer::connect(Dispatcher& dispatcher, ConnectionCallback callback) { - if (state_ == ConnectionState::CONNECTED) { - dispatcher.post( - [callback]() { callback(makeSuccess(nullptr)); }); - return; - } - - if (state_ == ConnectionState::CONNECTING) { - // Already connecting, queue the callback - pending_on_connect_.push_back( - [callback]() { callback(makeSuccess(nullptr)); }); - return; - } - - // Need to initialize - auto self = std::static_pointer_cast(Server::shared_from_this()); - initialize(dispatcher, [callback](Result result) { - if (mcp::holds_alternative(result)) { - callback(makeSuccess(nullptr)); - } else { - callback(Result(mcp::get(result))); - } - }); -} - -// Disconnect from the server -void MCPServer::disconnect(Dispatcher& dispatcher, - std::function callback) { - if (state_ == ConnectionState::DISCONNECTED) { - if (callback) { - dispatcher.post(callback); - } - return; - } - - state_ = ConnectionState::DISCONNECTED; - - if (client_) { - client_->disconnect(); - } - - if (callback) { - dispatcher.post(callback); - } -} - -// List available tools -void MCPServer::listTools(Dispatcher& dispatcher, - ServerToolListCallback callback) { - if (!this->Server::isConnected()) { - dispatcher.post([callback]() { - callback(makeOrchError>( - OrchError::NOT_CONNECTED, "Server is not connected")); - }); - return; - } - - // Return cached tools if available - if (!tools_.empty()) { - auto tools_copy = tools_; - dispatcher.post( - [callback, tools_copy]() { callback(makeSuccess(tools_copy)); }); - return; - } - - // Fetch tools from server - auto self = std::static_pointer_cast(Server::shared_from_this()); - auto tools_future_ptr = - std::make_shared>(client_->listTools()); - - dispatcher.post([self, callback, tools_future_ptr]() { - try { - mcp::ListToolsResult tools_result = tools_future_ptr->get(); - self->onToolsListed(tools_result); - callback(makeSuccess(self->tools_)); - } catch (const std::exception& e) { - callback(makeOrchError>( - OrchError::INTERNAL_ERROR, e.what())); - } - }); -} - -// Get a tool by name as a Runnable -JsonRunnablePtr MCPServer::tool(const std::string& name) { - // Check cache first - auto it = tool_cache_.find(name); - if (it != tool_cache_.end()) { - return it->second; - } - - // Find tool info - ServerToolInfo info; - bool found = false; - for (const auto& t : tools_) { - if (t.name == name) { - info = t; - found = true; - break; - } - } - - if (!found) { - // Create a placeholder tool info - info.name = name; - } - - // Create ServerTool wrapper - auto tool_ptr = - std::make_shared(Server::shared_from_this(), info); - tool_cache_[name] = tool_ptr; - return tool_ptr; -} - -// Call a tool directly -void MCPServer::callTool(const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) { - (void)config; // Config is handled internally by MCP client - - if (!this->Server::isConnected()) { - dispatcher.post([callback]() { - callback(makeOrchError(OrchError::NOT_CONNECTED, - "Server is not connected")); - }); - return; - } - - // Convert JsonValue to mcp::optional - mcp::optional mcp_args; - if (!arguments.isNull() && arguments.isObject()) { - // Use the existing jsonToMetadata function from json_serialization.h - mcp_args = mcp::json::jsonToMetadata(arguments); - } - - auto self = std::static_pointer_cast(Server::shared_from_this()); - - GOPHER_LOG_DEBUG("Initiating tool call: {}", name); - - auto tool_future_ptr = std::make_shared>( - client_->callTool(name, mcp_args)); - - GOPHER_LOG_DEBUG("Tool call future created, spawning wait thread for: {}", name); - - // Use a separate thread to wait on the future, then post result to dispatcher - // This avoids blocking the dispatcher thread which could cause deadlocks - std::thread([self, callback, tool_future_ptr, name, &dispatcher]() { - GOPHER_LOG_DEBUG("Wait thread started, awaiting MCP response for tool: {}", name); - try { - mcp::CallToolResult result = tool_future_ptr->get(); - GOPHER_LOG_DEBUG("MCP tool call completed for: {}", name); - - // Post the result back to the dispatcher - GOPHER_LOG_DEBUG("Posting result callback to dispatcher for tool: {}", name); - dispatcher.post([self, callback, result, name]() { - GOPHER_LOG_DEBUG("Processing tool result in dispatcher context for: {}", name); - if (result.isError) { - JsonValue error_content = contentToJson(result.content); - callback(makeOrchError(OrchError::INTERNAL_ERROR, - error_content.toString())); - } else { - JsonValue json_result = contentToJson(result.content); - callback(makeSuccess(json_result)); - } - GOPHER_LOG_DEBUG("Tool result callback completed for: {}", name); - }); - } catch (const std::exception& e) { - GOPHER_LOG_DEBUG("Exception in wait thread for tool {}: {}", name, e.what()); - dispatcher.post([callback, e]() { - callback(makeOrchError(OrchError::INTERNAL_ERROR, e.what())); - }); - } - }).detach(); - - GOPHER_LOG_DEBUG("Tool call initiated successfully, wait thread detached for: {}", name); -} - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/gopher/orch/server/rest_server.cc b/third_party/gopher-orch/src/gopher/orch/server/rest_server.cc deleted file mode 100644 index 460f8914..00000000 --- a/third_party/gopher-orch/src/gopher/orch/server/rest_server.cc +++ /dev/null @@ -1,408 +0,0 @@ -// RESTServer implementation -// -// Provides REST API access through the Server interface. -// The DefaultHttpClient provides a basic HTTP implementation. -// For production use, inject a custom HttpClient with a robust HTTP library. - -#include "gopher/orch/server/rest_server.h" - -#include -#include -#include -#include -#include - -namespace gopher { -namespace orch { -namespace server { - -namespace { - -// Atomic counter for generating unique IDs -std::atomic g_rest_id_counter{0}; - -// Parse URL into components -struct UrlComponents { - std::string scheme; // http or https - std::string host; - uint16_t port = 0; - std::string path; - std::string query; - - bool parse(const std::string& url) { - // Simple URL parser - // Format: scheme://host:port/path?query - - size_t scheme_end = url.find("://"); - if (scheme_end == std::string::npos) { - return false; - } - scheme = url.substr(0, scheme_end); - - size_t host_start = scheme_end + 3; - size_t path_start = url.find('/', host_start); - size_t query_start = url.find('?', host_start); - - std::string host_port; - if (path_start != std::string::npos) { - host_port = url.substr(host_start, path_start - host_start); - if (query_start != std::string::npos && query_start > path_start) { - path = url.substr(path_start, query_start - path_start); - query = url.substr(query_start + 1); - } else { - path = url.substr(path_start); - } - } else if (query_start != std::string::npos) { - host_port = url.substr(host_start, query_start - host_start); - query = url.substr(query_start + 1); - path = "/"; - } else { - host_port = url.substr(host_start); - path = "/"; - } - - // Parse host:port - size_t port_sep = host_port.find(':'); - if (port_sep != std::string::npos) { - host = host_port.substr(0, port_sep); - port = static_cast(std::stoi(host_port.substr(port_sep + 1))); - } else { - host = host_port; - port = (scheme == "https") ? 443 : 80; - } - - return true; - } -}; - -// Base64 encoding for basic auth -std::string base64Encode(const std::string& input) { - static const char* chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - std::string result; - result.reserve(((input.size() + 2) / 3) * 4); - - for (size_t i = 0; i < input.size(); i += 3) { - uint32_t n = static_cast(input[i]) << 16; - if (i + 1 < input.size()) - n |= static_cast(input[i + 1]) << 8; - if (i + 2 < input.size()) - n |= static_cast(input[i + 2]); - - result += chars[(n >> 18) & 0x3F]; - result += chars[(n >> 12) & 0x3F]; - result += (i + 1 < input.size()) ? chars[(n >> 6) & 0x3F] : '='; - result += (i + 2 < input.size()) ? chars[n & 0x3F] : '='; - } - - return result; -} - -// URL encode a string -std::string urlEncode(const std::string& value) { - std::ostringstream escaped; - escaped.fill('0'); - escaped << std::hex; - - for (char c : value) { - if (isalnum(static_cast(c)) || c == '-' || c == '_' || - c == '.' || c == '~') { - escaped << c; - } else { - escaped << '%' << std::setw(2) - << static_cast(static_cast(c)); - } - } - - return escaped.str(); -} - -} // namespace - -// ============================================================================= -// DefaultHttpClient Implementation -// ============================================================================= - -// DefaultHttpClient now uses the CurlHttpClient implementation -// This ensures all HTTP clients in the system use the same robust implementation -class DefaultHttpClient::Impl { - public: - Impl() { - // Create a CurlHttpClient instance to delegate to - http_client_ = createCurlHttpClient(); - } - - void request(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - HttpClient::ResponseCallback callback) { - // Delegate to CurlHttpClient - http_client_->request(method, url, headers, body, dispatcher, std::move(callback)); - } - - private: - std::shared_ptr http_client_; -}; - -DefaultHttpClient::DefaultHttpClient() : impl_(std::make_unique()) {} - -DefaultHttpClient::~DefaultHttpClient() = default; - -void DefaultHttpClient::request( - HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - ResponseCallback callback) { - impl_->request(method, url, headers, body, dispatcher, std::move(callback)); -} - -// ============================================================================= -// RESTServer Implementation -// ============================================================================= - -std::string RESTServer::generateId() { - std::ostringstream oss; - oss << "rest-server-" << ++g_rest_id_counter; - return oss.str(); -} - -RESTServer::RESTServer(const RESTServerConfig& config, - HttpClientPtr http_client) - : id_(generateId()), - config_(config), - http_client_(std::move(http_client)) {} - -RESTServer::~RESTServer() = default; - -RESTServer::Ptr RESTServer::create(const RESTServerConfig& config) { - auto http_client = std::make_shared(); - return create(config, http_client); -} - -RESTServer::Ptr RESTServer::create(const RESTServerConfig& config, - HttpClientPtr http_client) { - return std::shared_ptr( - new RESTServer(config, std::move(http_client))); -} - -void RESTServer::connect(Dispatcher& dispatcher, ConnectionCallback callback) { - // REST servers are stateless - no connection needed - // Just verify the configuration is valid - if (config_.base_url.empty()) { - dispatcher.post([callback]() { - callback(Result( - Error(OrchError::INVALID_ARGUMENT, "base_url is required"))); - }); - return; - } - - state_ = ConnectionState::CONNECTED; - dispatcher.post( - [callback]() { callback(core::makeSuccess(nullptr)); }); -} - -void RESTServer::disconnect(Dispatcher& dispatcher, - std::function callback) { - state_ = ConnectionState::DISCONNECTED; - if (callback) { - dispatcher.post(std::move(callback)); - } -} - -void RESTServer::listTools(Dispatcher& dispatcher, - ServerToolListCallback callback) { - std::vector tools; - tools.reserve(config_.tools.size()); - - for (const auto& entry : config_.tools) { - tools.push_back(entry.second.info); - } - - dispatcher.post([tools = std::move(tools), callback]() { - callback(core::makeSuccess(std::move(tools))); - }); -} - -JsonRunnablePtr RESTServer::tool(const std::string& name) { - std::lock_guard lock(mutex_); - - // Check cache - auto it = tool_cache_.find(name); - if (it != tool_cache_.end()) { - return it->second; - } - - // Find tool config - auto tool_it = config_.tools.find(name); - if (tool_it == config_.tools.end()) { - return nullptr; - } - - // Create ServerTool wrapper - auto tool_ptr = - std::make_shared(shared_from_this(), tool_it->second.info); - tool_cache_[name] = tool_ptr; - return tool_ptr; -} - -void RESTServer::callTool(const std::string& name, - const JsonValue& arguments, - const RunnableConfig& config, - Dispatcher& dispatcher, - JsonCallback callback) { - (void)config; // RunnableConfig not used for REST calls - - // Find tool endpoint - auto tool_it = config_.tools.find(name); - if (tool_it == config_.tools.end()) { - dispatcher.post([callback, name]() { - callback(Result( - Error(OrchError::TOOL_NOT_FOUND, "Tool not found: " + name))); - }); - return; - } - - const auto& endpoint = tool_it->second; - - // Build URL with path parameters - std::string url = buildUrl(endpoint.path, arguments); - - // Build headers - auto headers = buildHeaders(); - - // Add Content-Type for body - std::string body; - if (endpoint.send_body && !arguments.isNull()) { - headers["Content-Type"] = "application/json"; - body = arguments.toString(); - } - - // Make HTTP request - http_client_->request( - endpoint.method, url, headers, body, dispatcher, - [callback, endpoint](Result result) { - if (mcp::holds_alternative(result)) { - callback(Result(mcp::get(result))); - return; - } - - const auto& response = mcp::get(result); - - // Check for HTTP errors - if (!response.isSuccess()) { - std::ostringstream error_msg; - error_msg << "HTTP " << response.status_code; - if (!response.body.empty()) { - error_msg << ": " << response.body.substr(0, 200); - } - callback(Result( - Error(OrchError::INTERNAL_ERROR, error_msg.str()))); - return; - } - - // Parse response body as JSON - if (response.body.empty()) { - callback(core::makeSuccess(JsonValue::object())); - return; - } - - try { - JsonValue json_result = JsonValue::parse(response.body); - callback(core::makeSuccess(std::move(json_result))); - } catch (const std::exception& e) { - // Return raw body as string if not JSON - callback(core::makeSuccess(JsonValue(response.body))); - } - }); -} - -std::string RESTServer::buildUrl(const std::string& path, - const JsonValue& args) const { - std::string url = config_.base_url; - - // Replace path parameters - std::string result_path = path; - std::regex param_regex("\\{([^}]+)\\}"); - std::smatch match; - std::string::const_iterator search_start = result_path.cbegin(); - - std::string final_path; - size_t last_pos = 0; - - while ( - std::regex_search(search_start, result_path.cend(), match, param_regex)) { - std::string param_name = match[1].str(); - std::string replacement; - - // Get value from arguments - if (args.contains(param_name)) { - const JsonValue& value = args[param_name]; - if (value.isString()) { - replacement = urlEncode(value.getString()); - } else if (value.isInteger()) { - replacement = std::to_string(value.getInt()); - } else if (value.isFloat()) { - replacement = std::to_string(value.getFloat()); - } else if (value.isBoolean()) { - replacement = value.getBool() ? "true" : "false"; - } - } - - size_t match_start = static_cast(match.position()) + - (search_start - result_path.cbegin()); - final_path += result_path.substr(last_pos, match_start - last_pos); - final_path += replacement; - last_pos = match_start + match.length(); - - search_start = match.suffix().first; - } - - final_path += result_path.substr(last_pos); - - return url + final_path; -} - -std::map RESTServer::buildHeaders() const { - std::map headers = config_.default_headers; - - // Add authentication - switch (config_.auth.type) { - case RESTServerConfig::AuthConfig::Type::BEARER: - headers["Authorization"] = "Bearer " + config_.auth.bearer_token; - break; - case RESTServerConfig::AuthConfig::Type::BASIC: { - std::string credentials = - config_.auth.username + ":" + config_.auth.password; - headers["Authorization"] = "Basic " + base64Encode(credentials); - break; - } - case RESTServerConfig::AuthConfig::Type::API_KEY: - headers[config_.auth.api_key_header] = config_.auth.api_key; - break; - case RESTServerConfig::AuthConfig::Type::NONE: - default: - break; - } - - return headers; -} - -void RESTServer::setAuth(const RESTServerConfig::AuthConfig& auth) { - std::lock_guard lock(mutex_); - config_.auth = auth; -} - -void RESTServer::setDefaultHeader(const std::string& name, - const std::string& value) { - std::lock_guard lock(mutex_); - config_.default_headers[name] = value; -} - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/src/orch/hello.cc b/third_party/gopher-orch/src/orch/hello.cc deleted file mode 100644 index 9b05263c..00000000 --- a/third_party/gopher-orch/src/orch/hello.cc +++ /dev/null @@ -1,59 +0,0 @@ -#include "orch/core/hello.h" - -#include - -#include "orch/core/version.h" - -namespace gopher { -namespace orch { -namespace core { - -class Hello::Impl { - public: - Impl() : name_("World") {} - explicit Impl(const std::string& name) : name_(name) {} - - std::string name_; -}; - -Hello::Hello() : impl_(std::make_unique()) {} - -Hello::Hello(const std::string& name) : impl_(std::make_unique(name)) {} - -Hello::~Hello() = default; - -std::string Hello::greet() const { - std::ostringstream oss; - oss << "Hello, " << impl_->name_ << "!"; - return oss.str(); -} - -std::string Hello::greet_with_prefix(const std::string& prefix) const { - std::ostringstream oss; - oss << prefix << " " << impl_->name_ << "!"; - return oss.str(); -} - -void Hello::set_name(const std::string& name) { impl_->name_ = name; } - -const std::string& Hello::get_name() const { return impl_->name_; } - -std::string Hello::get_version() { return Version::string(); } - -HelloBuilder& HelloBuilder::with_name(const std::string& name) { - name_ = name; - return *this; -} - -HelloBuilder& HelloBuilder::with_greeting_style(const std::string& style) { - style_ = style; - return *this; -} - -std::unique_ptr HelloBuilder::build() const { - return std::make_unique(name_); -} - -} // namespace core -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/tests/CMakeLists.txt b/third_party/gopher-orch/tests/CMakeLists.txt deleted file mode 100644 index a9de9053..00000000 --- a/third_party/gopher-orch/tests/CMakeLists.txt +++ /dev/null @@ -1,145 +0,0 @@ -# gopher-orch tests - -# Test utilities and helpers -set(TEST_UTIL_SOURCES - # test_utils.cpp -) - -# Orch-specific core tests -set(ORCH_CORE_TEST_SOURCES - orch/hello_test.cpp -) - -# New gopher/orch framework tests - split by component -set(ORCH_FRAMEWORK_TEST_SOURCES - gopher/orch/lambda_test.cc - gopher/orch/sequence_test.cc - gopher/orch/parallel_test.cc - gopher/orch/router_test.cc - gopher/orch/retry_test.cc - gopher/orch/timeout_test.cc - gopher/orch/fallback_test.cc - gopher/orch/circuit_breaker_test.cc - gopher/orch/state_graph_test.cc - gopher/orch/state_machine_test.cc - gopher/orch/callback_manager_test.cc - gopher/orch/human_approval_test.cc - gopher/orch/mock_server_test.cc - gopher/orch/server_composite_test.cc - gopher/orch/mcp_server_test.cc - gopher/orch/rest_server_test.cc - gopher/orch/integration_test.cc - gopher/orch/tools_fetcher_integration_test.cpp -) - -# LLM, Agent, and ToolRegistry tests -set(ORCH_AGENT_TEST_SOURCES - gopher/orch/llm_provider_test.cc - gopher/orch/llm_runnable_test.cc - gopher/orch/agent_test.cc - gopher/orch/agent_state_test.cc - gopher/orch/agent_runnable_test.cc - gopher/orch/tool_registry_test.cc - gopher/orch/tool_runnable_test.cc - gopher/orch/tools_fetcher_test.cpp -) - -# FFI tests - organized by component -set(FFI_TEST_SOURCES - gopher/orch/FFI/ffi_types_test.cc - gopher/orch/FFI/ffi_error_test.cc - gopher/orch/FFI/ffi_handle_test.cc - gopher/orch/FFI/ffi_json_test.cc - gopher/orch/FFI/ffi_core_test.cc - gopher/orch/FFI/ffi_builder_test.cc - gopher/orch/FFI/ffi_raii_test.cc - gopher/orch/FFI/ffi_lambda_test.cc -) - -# Helper function to create orch test executables -function(add_orch_test test_name test_sources) - add_executable(${test_name} ${test_sources} ${TEST_UTIL_SOURCES}) - # Use static library for tests to avoid duplicate symbol issues - if(TARGET gopher-orch-static) - set(GOPHER_ORCH_TEST_LIB gopher-orch-static) - else() - set(GOPHER_ORCH_TEST_LIB gopher-orch) - endif() - target_link_libraries(${test_name} - ${GOPHER_ORCH_TEST_LIB} - ${GOPHER_MCP_LIBRARIES} - GTest::gtest - GTest::gtest_main - GTest::gmock - Threads::Threads - ) - target_include_directories(${test_name} PRIVATE - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_SOURCE_DIR}/tests - ${CMAKE_SOURCE_DIR}/tests/gopher/orch - ${CMAKE_SOURCE_DIR}/tests/gopher/orch/FFI - ${GOPHER_MCP_INCLUDE_DIR} - ) - - # Add test to CTest - gtest_discover_tests(${test_name} - WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} - PROPERTIES LABELS ${ARGN} - ) -endfunction() - -# Create individual orch test executables -add_orch_test(hello_test "${ORCH_CORE_TEST_SOURCES}" "orch") - -# Create orch framework test executable -add_orch_test(orch_framework_test "${ORCH_FRAMEWORK_TEST_SOURCES}" "orch-framework") - -# Create agent test executable (LLM, Agent, ToolRegistry) -add_orch_test(agent_test "${ORCH_AGENT_TEST_SOURCES}" "agent") - -# Create FFI test executable -add_orch_test(ffi_test "${FFI_TEST_SOURCES}" "ffi") - -# Create a combined orch test executable for convenience -add_executable(gopher-orch-tests - ${ORCH_CORE_TEST_SOURCES} - ${ORCH_FRAMEWORK_TEST_SOURCES} - ${ORCH_AGENT_TEST_SOURCES} - ${FFI_TEST_SOURCES} - ${TEST_UTIL_SOURCES} -) - -# Use static library for tests to avoid duplicate symbol issues -if(TARGET gopher-orch-static) - set(GOPHER_ORCH_TEST_LIB gopher-orch-static) -else() - set(GOPHER_ORCH_TEST_LIB gopher-orch) -endif() - -target_link_libraries(gopher-orch-tests - ${GOPHER_ORCH_TEST_LIB} - ${GOPHER_MCP_LIBRARIES} - GTest::gtest - GTest::gtest_main - GTest::gmock - Threads::Threads -) - -target_include_directories(gopher-orch-tests PRIVATE - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_SOURCE_DIR}/tests - ${CMAKE_SOURCE_DIR}/tests/gopher/orch - ${CMAKE_SOURCE_DIR}/tests/gopher/orch/FFI - ${GOPHER_MCP_INCLUDE_DIR} -) - -# Custom test targets -add_custom_target(test-verbose - COMMAND ${CMAKE_CTEST_COMMAND} -V - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} -) - -add_custom_target(test-parallel - COMMAND ${CMAKE_CTEST_COMMAND} -j${CMAKE_BUILD_PARALLEL_LEVEL} - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} -) diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_builder_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_builder_test.cc deleted file mode 100644 index 2ce134af..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_builder_test.cc +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file ffi_builder_test.cc - * @brief Unit tests for FFI builder components - * - * Tests: - * - SequenceImpl (Creation) - * - ParallelImpl (Creation) - * - RouterImpl (Creation) - * - TransactionImpl (Creation, AddAndCommit, Rollback) - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI Builder Tests -// ============================================================================= - -class FFIBuilderTest : public OrchTest { - protected: - void SetUp() override { - OrchTest::SetUp(); - ErrorManager::ClearError(); - } - - void TearDown() override { - ErrorManager::ClearError(); - OrchTest::TearDown(); - } -}; - -// ============================================================================= -// SequenceImpl Tests -// ============================================================================= - -TEST_F(FFIBuilderTest, SequenceImplCreation) { - auto* seq = new SequenceImpl(); - EXPECT_EQ(seq->GetType(), GOPHER_ORCH_TYPE_SEQUENCE); - EXPECT_TRUE(seq->steps.empty()); - seq->Release(); -} - -// ============================================================================= -// ParallelImpl Tests -// ============================================================================= - -TEST_F(FFIBuilderTest, ParallelImplCreation) { - auto* parallel = new ParallelImpl(); - EXPECT_EQ(parallel->GetType(), GOPHER_ORCH_TYPE_PARALLEL); - EXPECT_TRUE(parallel->branches.empty()); - parallel->Release(); -} - -// ============================================================================= -// RouterImpl Tests -// ============================================================================= - -TEST_F(FFIBuilderTest, RouterImplCreation) { - auto* router = new RouterImpl(); - EXPECT_EQ(router->GetType(), GOPHER_ORCH_TYPE_ROUTER); - EXPECT_TRUE(router->routes.empty()); - EXPECT_EQ(router->default_route, nullptr); - router->Release(); -} - -// ============================================================================= -// TransactionImpl Tests -// ============================================================================= - -TEST_F(FFIBuilderTest, TransactionImplCreation) { - auto* txn = new TransactionImpl(nullptr); - EXPECT_EQ(txn->GetType(), GOPHER_ORCH_TYPE_TRANSACTION); - EXPECT_EQ(txn->Size(), 0); - txn->Release(); -} - -TEST_F(FFIBuilderTest, TransactionImplAddAndCommit) { - auto* txn = new TransactionImpl(nullptr); - auto* json = new JsonImpl(core::JsonValue::object()); - - auto result = txn->Add(json, GOPHER_ORCH_TYPE_JSON); - EXPECT_EQ(result, GOPHER_ORCH_OK); - EXPECT_EQ(txn->Size(), 1); - - result = txn->Commit(); - EXPECT_EQ(result, GOPHER_ORCH_OK); - - /* After commit, json handle is still valid (ownership transferred) */ - json->Release(); - txn->Release(); -} - -TEST_F(FFIBuilderTest, TransactionImplRollback) { - auto* txn = new TransactionImpl(nullptr); - - /* Track a handle - it will be cleaned up on rollback */ - size_t initial_count = HandleRegistry::Instance().GetActiveCount(); - auto* json = new JsonImpl(core::JsonValue::object()); - EXPECT_EQ(HandleRegistry::Instance().GetActiveCount(), initial_count + 1); - - txn->Add(json, GOPHER_ORCH_TYPE_JSON); - txn->Rollback(); - - /* After rollback, json should be released */ - EXPECT_EQ(HandleRegistry::Instance().GetActiveCount(), initial_count); - - txn->Release(); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_core_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_core_test.cc deleted file mode 100644 index ad738d6e..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_core_test.cc +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @file ffi_core_test.cc - * @brief Unit tests for FFI core components - * - * Tests: - * - DispatcherImpl (Creation, Post) - * - ConfigImpl (Creation, WithTag) - * - CancelTokenImpl (Creation, Cancel) - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI Core Tests -// ============================================================================= - -class FFICoreTest : public OrchTest { - protected: - void SetUp() override { - OrchTest::SetUp(); - ErrorManager::ClearError(); - } - - void TearDown() override { - ErrorManager::ClearError(); - OrchTest::TearDown(); - } -}; - -// ============================================================================= -// DispatcherImpl Tests -// ============================================================================= - -TEST_F(FFICoreTest, DispatcherImplCreation) { - auto* dispatcher = new DispatcherImpl(); - EXPECT_NE(dispatcher->dispatcher, nullptr); - EXPECT_EQ(dispatcher->GetType(), GOPHER_ORCH_TYPE_DISPATCHER); - dispatcher->Release(); -} - -TEST_F(FFICoreTest, DispatcherImplPost) { - auto* dispatcher = new DispatcherImpl(); - std::atomic executed{false}; - - dispatcher->dispatcher->post([&executed]() { executed.store(true); }); - dispatcher->dispatcher->run(mcp::event::RunType::NonBlock); - - EXPECT_TRUE(executed.load()); - dispatcher->Release(); -} - -// ============================================================================= -// ConfigImpl Tests -// ============================================================================= - -TEST_F(FFICoreTest, ConfigImplCreation) { - auto* config = new ConfigImpl(); - EXPECT_EQ(config->GetType(), GOPHER_ORCH_TYPE_CONFIG); - config->Release(); -} - -TEST_F(FFICoreTest, ConfigImplWithTag) { - auto* config = new ConfigImpl(); - config->config.withTag("key", "value"); - EXPECT_TRUE(config->config.tag("key").has_value()); - EXPECT_EQ(config->config.tag("key").value(), "value"); - config->Release(); -} - -// ============================================================================= -// CancelTokenImpl Tests -// ============================================================================= - -TEST_F(FFICoreTest, CancelTokenImplCreation) { - auto* token = new CancelTokenImpl(); - EXPECT_EQ(token->GetType(), GOPHER_ORCH_TYPE_CANCEL_TOKEN); - EXPECT_FALSE(token->cancelled.load()); - token->Release(); -} - -TEST_F(FFICoreTest, CancelTokenImplCancel) { - auto* token = new CancelTokenImpl(); - EXPECT_FALSE(token->cancelled.load()); - - token->cancelled.store(true); - EXPECT_TRUE(token->cancelled.load()); - - token->Release(); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_error_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_error_test.cc deleted file mode 100644 index 63e6efb2..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_error_test.cc +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @file ffi_error_test.cc - * @brief Unit tests for FFI error handling - * - * Tests: - * - ErrorManager SetAndGet - * - ErrorManager Clear - * - ErrorManager GetName - * - Error scope pattern - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI Error Tests -// ============================================================================= - -class FFIErrorTest : public OrchTest { - protected: - void SetUp() override { - OrchTest::SetUp(); - ErrorManager::ClearError(); - } - - void TearDown() override { - ErrorManager::ClearError(); - OrchTest::TearDown(); - } -}; - -// ============================================================================= -// Error Manager Tests -// ============================================================================= - -TEST_F(FFIErrorTest, ErrorManagerSetAndGet) { - ErrorManager::SetError(GOPHER_ORCH_ERROR_INVALID_ARGUMENT, "Test error", - "Detail info"); - - auto* info = ErrorManager::GetLastError(); - ASSERT_NE(info, nullptr); - EXPECT_EQ(info->code, GOPHER_ORCH_ERROR_INVALID_ARGUMENT); - EXPECT_STREQ(info->message, "Test error"); - EXPECT_STREQ(info->details, "Detail info"); -} - -TEST_F(FFIErrorTest, ErrorManagerClear) { - ErrorManager::SetError(GOPHER_ORCH_ERROR_TIMEOUT, "Error"); - EXPECT_NE(ErrorManager::GetLastError(), nullptr); - - ErrorManager::ClearError(); - EXPECT_EQ(ErrorManager::GetLastError(), nullptr); -} - -TEST_F(FFIErrorTest, ErrorManagerGetName) { - EXPECT_STREQ(ErrorManager::GetErrorName(GOPHER_ORCH_OK), "GOPHER_ORCH_OK"); - EXPECT_STREQ(ErrorManager::GetErrorName(GOPHER_ORCH_ERROR_TIMEOUT), - "GOPHER_ORCH_ERROR_TIMEOUT"); - EXPECT_STREQ(ErrorManager::GetErrorName(GOPHER_ORCH_ERROR_CANCELLED), - "GOPHER_ORCH_ERROR_CANCELLED"); - EXPECT_STREQ(ErrorManager::GetErrorName(GOPHER_ORCH_ERROR_INVALID_HANDLE), - "GOPHER_ORCH_ERROR_INVALID_HANDLE"); - EXPECT_STREQ( - ErrorManager::GetErrorName(static_cast(-9999)), - "GOPHER_ORCH_ERROR_UNKNOWN"); -} - -// ============================================================================= -// Error Scope Pattern Tests -// ============================================================================= - -TEST_F(FFIErrorTest, ErrorScopePattern) { - /* Test the error scope pattern using ErrorManager directly */ - ErrorManager::SetError(GOPHER_ORCH_ERROR_TIMEOUT, "Pre-existing error"); - - { - /* Clear error on entry (what ErrorScope does) */ - ErrorManager::ClearError(); - - /* Verify error is cleared */ - EXPECT_EQ(ErrorManager::GetLastError(), nullptr); - - /* Set a new error */ - ErrorManager::SetError(GOPHER_ORCH_ERROR_CANCELLED, "New error"); - - /* Verify new error */ - auto* info = ErrorManager::GetLastError(); - ASSERT_NE(info, nullptr); - EXPECT_EQ(info->code, GOPHER_ORCH_ERROR_CANCELLED); - EXPECT_STREQ(info->message, "New error"); - } -} diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_handle_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_handle_test.cc deleted file mode 100644 index 442de2ff..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_handle_test.cc +++ /dev/null @@ -1,159 +0,0 @@ -/** - * @file ffi_handle_test.cc - * @brief Unit tests for FFI handle management - * - * Tests: - * - Handle registry (Basic, InvalidHandle, Stats) - * - Handle base (RefCounting) - * - GuardImpl (Creation, WithCleanup, Release) - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI Handle Tests -// ============================================================================= - -class FFIHandleTest : public OrchTest { - protected: - void SetUp() override { - OrchTest::SetUp(); - ErrorManager::ClearError(); - } - - void TearDown() override { - ErrorManager::ClearError(); - OrchTest::TearDown(); - } -}; - -// ============================================================================= -// Handle Registry Tests -// ============================================================================= - -TEST_F(FFIHandleTest, HandleRegistryBasic) { - size_t initial_count = HandleRegistry::Instance().GetActiveCount(); - - { - /* Create a JsonImpl handle */ - auto* json = new JsonImpl(core::JsonValue::object()); - EXPECT_EQ(HandleRegistry::Instance().GetActiveCount(), initial_count + 1); - EXPECT_TRUE(HandleRegistry::Instance().IsValid(json)); - - json->Release(); - } - - EXPECT_EQ(HandleRegistry::Instance().GetActiveCount(), initial_count); -} - -TEST_F(FFIHandleTest, HandleRegistryInvalidHandle) { - EXPECT_FALSE(HandleRegistry::Instance().IsValid(nullptr)); - EXPECT_FALSE( - HandleRegistry::Instance().IsValid(reinterpret_cast(0x1234))); -} - -TEST_F(FFIHandleTest, HandleRegistryStats) { - auto stats_before = HandleRegistry::Instance().GetStats(); - - { - auto* json = new JsonImpl(core::JsonValue::null()); - json->Release(); - } - - auto stats_after = HandleRegistry::Instance().GetStats(); - EXPECT_EQ(stats_after.total_created, stats_before.total_created + 1); - EXPECT_EQ(stats_after.total_destroyed, stats_before.total_destroyed + 1); -} - -// ============================================================================= -// Handle Base Tests -// ============================================================================= - -TEST_F(FFIHandleTest, HandleBaseRefCounting) { - auto* json = new JsonImpl(core::JsonValue::object()); - EXPECT_EQ(json->GetRefCount(), 1); - EXPECT_EQ(json->GetType(), GOPHER_ORCH_TYPE_JSON); - - json->AddRef(); - EXPECT_EQ(json->GetRefCount(), 2); - - json->Release(); - EXPECT_EQ(json->GetRefCount(), 1); - - json->Release(); /* Should delete */ -} - -// ============================================================================= -// GuardImpl Tests -// ============================================================================= - -TEST_F(FFIHandleTest, GuardImplCreation) { - /* Test that GuardImpl is created with correct type */ - auto* guard = new GuardImpl(reinterpret_cast(0x1234), - GOPHER_ORCH_TYPE_JSON, nullptr); - - EXPECT_EQ(guard->GetType(), GOPHER_ORCH_TYPE_GUARD); - EXPECT_EQ(guard->handle_, reinterpret_cast(0x1234)); - EXPECT_EQ(guard->type_, GOPHER_ORCH_TYPE_JSON); - EXPECT_EQ(guard->cleanup_, nullptr); - EXPECT_FALSE(guard->released_); - - /* Use HandleBase::Release to decrement refcount and delete */ - guard->HandleBase::Release(); -} - -TEST_F(FFIHandleTest, GuardImplWithCleanup) { - /* Test cleanup function is called when guard is destroyed */ - static bool cleanup_called = false; - static void* cleanup_ptr = nullptr; - - /* Use a struct to hold the state and provide a static function */ - struct CleanupState { - static void cleanup(void* ptr) { - cleanup_called = true; - cleanup_ptr = ptr; - } - }; - - cleanup_called = false; - cleanup_ptr = nullptr; - - { - auto* guard = new GuardImpl(reinterpret_cast(0x5678), - GOPHER_ORCH_TYPE_JSON, CleanupState::cleanup); - - EXPECT_EQ(guard->GetRefCount(), 1); - /* Use HandleBase::Release to decrement refcount and trigger destructor */ - guard->HandleBase::Release(); - } - - EXPECT_TRUE(cleanup_called); - EXPECT_EQ(cleanup_ptr, reinterpret_cast(0x5678)); -} - -/* Static for GuardImplRelease test */ -static bool g_guard_release_cleanup_called = false; - -static void guard_release_cleanup_fn(void*) { - g_guard_release_cleanup_called = true; -} - -TEST_F(FFIHandleTest, GuardImplRelease) { - g_guard_release_cleanup_called = false; - - auto* guard = - new GuardImpl(reinterpret_cast(0x5678), GOPHER_ORCH_TYPE_UNKNOWN, - guard_release_cleanup_fn); - - void* ptr = guard->Release(); - EXPECT_EQ(ptr, reinterpret_cast(0x5678)); - - guard->HandleBase::Release(); - - /* Cleanup should NOT be called since we released ownership */ - EXPECT_FALSE(g_guard_release_cleanup_called); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_json_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_json_test.cc deleted file mode 100644 index b619e0f9..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_json_test.cc +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @file ffi_json_test.cc - * @brief Unit tests for FFI JSON handling - * - * Tests: - * - JsonImpl (Null, Object, Array) - * - IteratorImpl (ObjectIteration, ArrayIteration) - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI JSON Tests -// ============================================================================= - -class FFIJsonTest : public OrchTest { - protected: - void SetUp() override { - OrchTest::SetUp(); - ErrorManager::ClearError(); - } - - void TearDown() override { - ErrorManager::ClearError(); - OrchTest::TearDown(); - } -}; - -// ============================================================================= -// JsonImpl Tests -// ============================================================================= - -TEST_F(FFIJsonTest, JsonImplNull) { - auto* json = new JsonImpl(core::JsonValue::null()); - EXPECT_TRUE(json->value.isNull()); - json->Release(); -} - -TEST_F(FFIJsonTest, JsonImplObject) { - auto* json = new JsonImpl(core::JsonValue::object()); - EXPECT_TRUE(json->value.isObject()); - json->value["key"] = core::JsonValue("value"); - EXPECT_EQ(json->value["key"].getString(), "value"); - json->Release(); -} - -TEST_F(FFIJsonTest, JsonImplArray) { - auto* json = new JsonImpl(core::JsonValue::array()); - EXPECT_TRUE(json->value.isArray()); - json->value.push_back(core::JsonValue(1)); - json->value.push_back(core::JsonValue(2)); - EXPECT_EQ(json->value.size(), 2); - json->Release(); -} - -// ============================================================================= -// IteratorImpl Tests -// ============================================================================= - -TEST_F(FFIJsonTest, IteratorImplObjectIteration) { - auto* json = new JsonImpl(core::JsonValue::object()); - json->value["a"] = core::JsonValue(1); - json->value["b"] = core::JsonValue(2); - - auto* iter = new IteratorImpl(reinterpret_cast(json)); - EXPECT_EQ(iter->GetType(), GOPHER_ORCH_TYPE_ITERATOR); - EXPECT_TRUE(iter->is_object_); - EXPECT_EQ(iter->object_keys_.size(), 2); - - iter->Release(); - json->Release(); -} - -TEST_F(FFIJsonTest, IteratorImplArrayIteration) { - auto* json = new JsonImpl(core::JsonValue::array()); - json->value.push_back(core::JsonValue(1)); - json->value.push_back(core::JsonValue(2)); - json->value.push_back(core::JsonValue(3)); - - auto* iter = new IteratorImpl(reinterpret_cast(json)); - EXPECT_FALSE(iter->is_object_); - EXPECT_EQ(iter->array_size_, 3); - - iter->Release(); - json->Release(); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_lambda_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_lambda_test.cc deleted file mode 100644 index 4ac86d8f..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_lambda_test.cc +++ /dev/null @@ -1,112 +0,0 @@ -/** - * @file ffi_lambda_test.cc - * @brief Unit tests for FFI lambda and callback components - * - * Tests: - * - LambdaRunnable (Creation, WithContext, Destructor) - * - CallbackManagerImpl (Creation) - * - ApprovalHandlerImpl (Creation) - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI Lambda Tests -// ============================================================================= - -class FFILambdaTest : public OrchTest { - protected: - void SetUp() override { - OrchTest::SetUp(); - ErrorManager::ClearError(); - } - - void TearDown() override { - ErrorManager::ClearError(); - OrchTest::TearDown(); - } -}; - -// ============================================================================= -// LambdaRunnable Tests -// ============================================================================= - -TEST_F(FFILambdaTest, LambdaRunnableCreation) { - auto runnable = std::make_shared( - [](void*, gopher_orch_json_t input, - gopher_orch_error_t* out_error) -> gopher_orch_json_t { - (void)input; - *out_error = GOPHER_ORCH_OK; - return reinterpret_cast( - new JsonImpl(core::JsonValue(42))); - }, - nullptr, nullptr, "TestLambda"); - - EXPECT_EQ(runnable->name(), "TestLambda"); -} - -TEST_F(FFILambdaTest, LambdaRunnableWithContext) { - int context_value = 100; - - auto runnable = std::make_shared( - [](void* ctx, gopher_orch_json_t, - gopher_orch_error_t* out_error) -> gopher_orch_json_t { - int* value = static_cast(ctx); - *out_error = GOPHER_ORCH_OK; - return reinterpret_cast( - new JsonImpl(core::JsonValue(*value))); - }, - &context_value, nullptr, "ContextLambda"); - - EXPECT_EQ(runnable->name(), "ContextLambda"); -} - -TEST_F(FFILambdaTest, LambdaRunnableDestructor) { - static bool destructor_called = false; - destructor_called = false; - - { - auto runnable = std::make_shared( - [](void*, gopher_orch_json_t, - gopher_orch_error_t* out_error) -> gopher_orch_json_t { - *out_error = GOPHER_ORCH_OK; - return reinterpret_cast( - new JsonImpl(core::JsonValue::null())); - }, - reinterpret_cast(0x1234), - [](void* ctx) { - EXPECT_EQ(ctx, reinterpret_cast(0x1234)); - destructor_called = true; - }, - "DestructorLambda"); - } - - EXPECT_TRUE(destructor_called); -} - -// ============================================================================= -// CallbackManagerImpl Tests -// ============================================================================= - -TEST_F(FFILambdaTest, CallbackManagerImplCreation) { - auto* manager = new CallbackManagerImpl(); - EXPECT_EQ(manager->GetType(), GOPHER_ORCH_TYPE_CALLBACK_MANAGER); - EXPECT_NE(manager->manager, nullptr); - manager->Release(); -} - -// ============================================================================= -// ApprovalHandlerImpl Tests -// ============================================================================= - -TEST_F(FFILambdaTest, ApprovalHandlerImplCreation) { - auto handler = std::make_shared("Test approval"); - auto* impl = new ApprovalHandlerImpl(handler); - EXPECT_EQ(impl->GetType(), GOPHER_ORCH_TYPE_APPROVAL_HANDLER); - EXPECT_NE(impl->handler, nullptr); - impl->Release(); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_raii_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_raii_test.cc deleted file mode 100644 index fadcaf32..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_raii_test.cc +++ /dev/null @@ -1,236 +0,0 @@ -/** - * @file ffi_raii_test.cc - * @brief Unit tests for FFI RAII utilities - * - * Tests: - * - ResourceGuard (Basic, Release, Move, Reset, Swap) - * - AllocationTransaction (Commit, Rollback, ExplicitRollback, Move) - * - ScopedCleanup (Basic, Dismiss, Execute, Move) - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_raii.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI RAII Tests -// ============================================================================= - -class FFIRaiiTest : public OrchTest { - protected: - void SetUp() override { - OrchTest::SetUp(); - ErrorManager::ClearError(); - } - - void TearDown() override { - ErrorManager::ClearError(); - OrchTest::TearDown(); - } -}; - -// ============================================================================= -// ResourceGuard Tests -// ============================================================================= - -TEST_F(FFIRaiiTest, ResourceGuardBasic) { - static bool released = false; - released = false; - - { - ResourceGuard guard(reinterpret_cast(0x1234), [](void* ptr) { - EXPECT_EQ(ptr, reinterpret_cast(0x1234)); - released = true; - }); - - EXPECT_TRUE(static_cast(guard)); - EXPECT_EQ(guard.get(), reinterpret_cast(0x1234)); - } - - EXPECT_TRUE(released); -} - -TEST_F(FFIRaiiTest, ResourceGuardRelease) { - static bool released = false; - released = false; - - void* ptr = nullptr; - { - ResourceGuard guard(reinterpret_cast(0x5678), - [](void*) { released = true; }); - - ptr = guard.release(); - } - - EXPECT_FALSE(released); - EXPECT_EQ(ptr, reinterpret_cast(0x5678)); -} - -TEST_F(FFIRaiiTest, ResourceGuardMove) { - static int release_count = 0; - release_count = 0; - - { - ResourceGuard guard1(reinterpret_cast(0xABCD), - [](void*) { release_count++; }); - - ResourceGuard guard2 = std::move(guard1); - - EXPECT_FALSE(static_cast(guard1)); - EXPECT_TRUE(static_cast(guard2)); - } - - EXPECT_EQ(release_count, 1); -} - -TEST_F(FFIRaiiTest, ResourceGuardReset) { - static int release_count = 0; - release_count = 0; - - ResourceGuard guard(reinterpret_cast(0x1111), - [](void*) { release_count++; }); - - guard.reset(reinterpret_cast(0x2222)); - EXPECT_EQ(release_count, 1); - EXPECT_EQ(guard.get(), reinterpret_cast(0x2222)); - - guard.reset(); - EXPECT_EQ(release_count, 2); - EXPECT_FALSE(static_cast(guard)); -} - -TEST_F(FFIRaiiTest, ResourceGuardSwap) { - ResourceGuard guard1(reinterpret_cast(0x1111), [](void*) {}); - ResourceGuard guard2(reinterpret_cast(0x2222), [](void*) {}); - - guard1.swap(guard2); - - EXPECT_EQ(guard1.get(), reinterpret_cast(0x2222)); - EXPECT_EQ(guard2.get(), reinterpret_cast(0x1111)); -} - -// ============================================================================= -// AllocationTransaction Tests -// ============================================================================= - -TEST_F(FFIRaiiTest, AllocationTransactionCommit) { - static int cleanup_count = 0; - cleanup_count = 0; - - { - AllocationTransaction txn; - txn.track(reinterpret_cast(1), [](void*) { cleanup_count++; }); - txn.track(reinterpret_cast(2), [](void*) { cleanup_count++; }); - - EXPECT_EQ(txn.size(), 2); - txn.commit(); - EXPECT_TRUE(txn.is_committed()); - } - - /* After commit, resources should NOT be cleaned up */ - EXPECT_EQ(cleanup_count, 0); -} - -TEST_F(FFIRaiiTest, AllocationTransactionRollback) { - static int cleanup_count = 0; - cleanup_count = 0; - - { - AllocationTransaction txn; - txn.track(reinterpret_cast(1), [](void*) { cleanup_count++; }); - txn.track(reinterpret_cast(2), [](void*) { cleanup_count++; }); - /* No commit - should rollback on destruction */ - } - - /* After rollback, all resources should be cleaned up */ - EXPECT_EQ(cleanup_count, 2); -} - -TEST_F(FFIRaiiTest, AllocationTransactionExplicitRollback) { - static int cleanup_count = 0; - cleanup_count = 0; - - AllocationTransaction txn; - txn.track(reinterpret_cast(1), [](void*) { cleanup_count++; }); - txn.track(reinterpret_cast(2), [](void*) { cleanup_count++; }); - - txn.rollback(); - EXPECT_EQ(cleanup_count, 2); - EXPECT_EQ(txn.size(), 0); - EXPECT_TRUE( - txn.is_committed()); /* Marked as committed to prevent double cleanup */ -} - -TEST_F(FFIRaiiTest, AllocationTransactionMove) { - static int cleanup_count = 0; - cleanup_count = 0; - - { - AllocationTransaction txn1; - txn1.track(reinterpret_cast(1), [](void*) { cleanup_count++; }); - - AllocationTransaction txn2 = std::move(txn1); - EXPECT_EQ(txn2.size(), 1); - /* txn1 should not cleanup since ownership moved */ - } - - EXPECT_EQ(cleanup_count, 1); /* Only txn2 cleaned up */ -} - -// ============================================================================= -// ScopedCleanup Tests -// ============================================================================= - -TEST_F(FFIRaiiTest, ScopedCleanupBasic) { - static bool cleaned = false; - cleaned = false; - - { - ScopedCleanup cleanup([&]() { cleaned = true; }); - } - - EXPECT_TRUE(cleaned); -} - -TEST_F(FFIRaiiTest, ScopedCleanupDismiss) { - static bool cleaned = false; - cleaned = false; - - { - ScopedCleanup cleanup([&]() { cleaned = true; }); - cleanup.dismiss(); - } - - EXPECT_FALSE(cleaned); -} - -TEST_F(FFIRaiiTest, ScopedCleanupExecute) { - static bool cleaned = false; - cleaned = false; - - { - ScopedCleanup cleanup([&]() { cleaned = true; }); - cleanup.execute(); - EXPECT_TRUE(cleaned); - } - - /* Should not execute twice */ - cleaned = false; - /* Destructor runs but cleanup was already dismissed */ -} - -TEST_F(FFIRaiiTest, ScopedCleanupMove) { - static int cleanup_count = 0; - cleanup_count = 0; - - { - ScopedCleanup cleanup1([&]() { cleanup_count++; }); - ScopedCleanup cleanup2 = std::move(cleanup1); - /* cleanup1 should not cleanup since ownership moved */ - } - - EXPECT_EQ(cleanup_count, 1); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_types_test.cc b/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_types_test.cc deleted file mode 100644 index ec87cfc0..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/FFI/ffi_types_test.cc +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @file ffi_types_test.cc - * @brief Unit tests for FFI type definitions and configuration structures - * - * Tests: - * - Version macros - * - Boolean constants - * - Error code values - * - Type ID values - * - Channel type values - * - Transport type values - * - Configuration structures (RetryPolicy, CircuitBreaker, McpConfig, etc.) - */ - -#include "gopher/orch/ffi/orch_ffi_bridge.h" -#include "gopher/orch/ffi/orch_ffi_types.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::ffi; - -// ============================================================================= -// Test Fixture for FFI Type Tests -// ============================================================================= - -class FFITypesTest : public OrchTest {}; - -// ============================================================================= -// Version and Constant Tests -// ============================================================================= - -TEST_F(FFITypesTest, VersionMacros) { - EXPECT_GE(GOPHER_ORCH_VERSION_MAJOR, 1); - EXPECT_GE(GOPHER_ORCH_VERSION_MINOR, 0); - EXPECT_GE(GOPHER_ORCH_VERSION_PATCH, 0); -} - -TEST_F(FFITypesTest, BooleanConstants) { - EXPECT_EQ(GOPHER_ORCH_FALSE, 0); - EXPECT_NE(GOPHER_ORCH_TRUE, 0); -} - -TEST_F(FFITypesTest, ErrorCodeValues) { - EXPECT_EQ(GOPHER_ORCH_OK, 0); - EXPECT_LT(GOPHER_ORCH_ERROR_INVALID_HANDLE, 0); - EXPECT_LT(GOPHER_ORCH_ERROR_INVALID_ARGUMENT, 0); - EXPECT_LT(GOPHER_ORCH_ERROR_NULL_POINTER, 0); - EXPECT_LT(GOPHER_ORCH_ERROR_NOT_FOUND, 0); - EXPECT_LT(GOPHER_ORCH_ERROR_TIMEOUT, 0); - EXPECT_LT(GOPHER_ORCH_ERROR_CANCELLED, 0); -} - -TEST_F(FFITypesTest, TypeIdValues) { - EXPECT_NE(GOPHER_ORCH_TYPE_DISPATCHER, GOPHER_ORCH_TYPE_RUNNABLE); - EXPECT_NE(GOPHER_ORCH_TYPE_JSON, GOPHER_ORCH_TYPE_CONFIG); - EXPECT_NE(GOPHER_ORCH_TYPE_FSM, GOPHER_ORCH_TYPE_GRAPH); -} - -TEST_F(FFITypesTest, ChannelTypeValues) { - EXPECT_EQ(GOPHER_ORCH_CHANNEL_LAST_VALUE, 0); - EXPECT_EQ(GOPHER_ORCH_CHANNEL_APPEND_LIST, 1); - EXPECT_EQ(GOPHER_ORCH_CHANNEL_MERGE_OBJECT, 2); -} - -TEST_F(FFITypesTest, TransportTypeValues) { - EXPECT_EQ(GOPHER_ORCH_TRANSPORT_STDIO, 0); - EXPECT_EQ(GOPHER_ORCH_TRANSPORT_SSE, 1); - EXPECT_EQ(GOPHER_ORCH_TRANSPORT_WEBSOCKET, 2); -} - -// ============================================================================= -// Configuration Structure Tests -// ============================================================================= - -TEST_F(FFITypesTest, RetryPolicyStructure) { - gopher_orch_retry_policy_t policy = {}; - policy.max_attempts = 3; - policy.initial_delay_ms = 100; - policy.backoff_multiplier = 2.0; - policy.max_delay_ms = 1000; - policy.jitter = GOPHER_ORCH_TRUE; - - EXPECT_EQ(policy.max_attempts, 3); - EXPECT_EQ(policy.initial_delay_ms, 100); - EXPECT_DOUBLE_EQ(policy.backoff_multiplier, 2.0); - EXPECT_EQ(policy.max_delay_ms, 1000); - EXPECT_EQ(policy.jitter, GOPHER_ORCH_TRUE); -} - -TEST_F(FFITypesTest, CircuitBreakerPolicyStructure) { - gopher_orch_circuit_breaker_policy_t policy = {}; - policy.failure_threshold = 5; - policy.recovery_timeout_ms = 30000; - policy.half_open_max_calls = 1; - - EXPECT_EQ(policy.failure_threshold, 5); - EXPECT_EQ(policy.recovery_timeout_ms, 30000); - EXPECT_EQ(policy.half_open_max_calls, 1); -} - -TEST_F(FFITypesTest, McpConfigStructure) { - gopher_orch_mcp_config_t config = {}; - - config.name = "test-server"; - config.transport = GOPHER_ORCH_TRANSPORT_STDIO; - config.command = "/usr/bin/echo"; - config.connect_timeout_ms = 5000; - config.request_timeout_ms = 30000; - - EXPECT_STREQ(config.name, "test-server"); - EXPECT_EQ(config.transport, GOPHER_ORCH_TRANSPORT_STDIO); - EXPECT_STREQ(config.command, "/usr/bin/echo"); - EXPECT_EQ(config.connect_timeout_ms, 5000); - EXPECT_EQ(config.request_timeout_ms, 30000); -} - -TEST_F(FFITypesTest, TransactionOptsStructure) { - gopher_orch_transaction_opts_t opts = {}; - opts.auto_rollback = GOPHER_ORCH_TRUE; - opts.strict_ordering = GOPHER_ORCH_TRUE; - opts.max_resources = 100; - - EXPECT_EQ(opts.auto_rollback, GOPHER_ORCH_TRUE); - EXPECT_EQ(opts.strict_ordering, GOPHER_ORCH_TRUE); - EXPECT_EQ(opts.max_resources, 100); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/agent_runnable_test.cc b/third_party/gopher-orch/tests/gopher/orch/agent_runnable_test.cc deleted file mode 100644 index 8819e32c..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/agent_runnable_test.cc +++ /dev/null @@ -1,483 +0,0 @@ -// Unit tests for AgentRunnable - -#include "gopher/orch/agent/agent_runnable.h" - -#include "mock_llm_provider.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -// ============================================================================= -// AgentRunnable Test Fixture -// ============================================================================= - -class AgentRunnableTest : public OrchTest { - protected: - std::shared_ptr mock_provider_; - ToolRegistryPtr registry_; - ToolExecutorPtr executor_; - AgentRunnable::Ptr agent_; - - void SetUp() override { - OrchTest::SetUp(); - mock_provider_ = makeMockLLMProvider("test-llm"); - registry_ = makeToolRegistry(); - executor_ = makeToolExecutor(registry_); - - addTestTools(); - - agent_ = AgentRunnable::create( - mock_provider_, executor_, - AgentConfig("gpt-4").withSystemPrompt("You are a helpful assistant.")); - } - - void addTestTools() { - // Search tool - registry_->addTool( - "search", "Search the web", JsonValue::object(), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - std::string query = "default"; - if (args.contains("query") && args["query"].isString()) { - query = args["query"].getString(); - } - - JsonValue result = JsonValue::object(); - result["query"] = query; - result["answer"] = "Search result for: " + query; - - d.post([cb = std::move(cb), result = std::move(result)]() mutable { - cb(Result(std::move(result))); - }); - }); - - // Calculator tool - registry_->addSyncTool( - "calculator", "Perform calculations", JsonValue::object(), - [](const JsonValue& args) -> Result { - if (args.contains("expression") && args["expression"].isString()) { - std::string expr = args["expression"].getString(); - if (expr == "2+2") { - return Result(JsonValue(4)); - } - } - return Result(JsonValue(0)); - }); - } -}; - -// ============================================================================= -// Basic Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, Name) { EXPECT_EQ(agent_->name(), "AgentRunnable"); } - -TEST_F(AgentRunnableTest, Accessors) { - EXPECT_EQ(agent_->provider(), mock_provider_); - EXPECT_EQ(agent_->executor(), executor_); - EXPECT_EQ(agent_->registry(), registry_); -} - -// ============================================================================= -// Simple Query Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, SimpleQueryNoTools) { - mock_provider_->setDefaultResponse("Hello! How can I help you?"); - - JsonValue input = "Hi there!"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result.isObject()); - EXPECT_EQ(result["status"].getString(), "completed"); - EXPECT_EQ(result["response"].getString(), "Hello! How can I help you?"); - EXPECT_EQ(result["iterations"].getInt(), 1); - - // Check messages include system prompt - auto last_msgs = mock_provider_->lastMessages(); - EXPECT_GE(last_msgs.size(), 2u); - EXPECT_EQ(last_msgs[0].role, Role::SYSTEM); - EXPECT_EQ(last_msgs[0].content, "You are a helpful assistant."); -} - -TEST_F(AgentRunnableTest, QueryObjectInput) { - mock_provider_->setDefaultResponse("The weather is sunny."); - - JsonValue input = JsonValue::object(); - input["query"] = "What is the weather?"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["status"].getString(), "completed"); - EXPECT_EQ(result["response"].getString(), "The weather is sunny."); -} - -// ============================================================================= -// Tool Usage Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, SingleToolCall) { - // First response: call search tool - std::vector tool_calls; - JsonValue args = JsonValue::object(); - args["query"] = "weather in tokyo"; - tool_calls.push_back(ToolCall("call_1", "search", args)); - mock_provider_->queueToolCalls(tool_calls); - - // Second response: final answer - mock_provider_->queueResponse( - "Based on the search, the weather in Tokyo is sunny."); - - JsonValue input = "What is the weather in Tokyo?"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["status"].getString(), "completed"); - EXPECT_EQ(result["response"].getString(), - "Based on the search, the weather in Tokyo is sunny."); - EXPECT_EQ(result["iterations"].getInt(), 2); - - // Verify tool results were added to conversation - EXPECT_TRUE(result["messages"].isArray()); - bool found_tool_result = false; - for (size_t i = 0; i < result["messages"].size(); ++i) { - if (result["messages"][i]["role"].getString() == "tool") { - found_tool_result = true; - break; - } - } - EXPECT_TRUE(found_tool_result); -} - -TEST_F(AgentRunnableTest, MultipleToolCalls) { - // First response: call two tools - std::vector tool_calls; - JsonValue args1 = JsonValue::object(); - args1["query"] = "weather"; - tool_calls.push_back(ToolCall("call_1", "search", args1)); - - JsonValue args2 = JsonValue::object(); - args2["expression"] = "2+2"; - tool_calls.push_back(ToolCall("call_2", "calculator", args2)); - - mock_provider_->queueToolCalls(tool_calls); - - // Second response: final answer - mock_provider_->queueResponse("I found weather info and calculated 2+2=4."); - - JsonValue input = "Search weather and calculate 2+2"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["status"].getString(), "completed"); - EXPECT_EQ(result["iterations"].getInt(), 2); -} - -// ============================================================================= -// Configuration Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, ConfigOverridesInInput) { - mock_provider_->setDefaultResponse("OK"); - - JsonValue input = JsonValue::object(); - input["query"] = "Test"; - - JsonValue config = JsonValue::object(); - config["system_prompt"] = "Custom system prompt"; - config["model"] = "gpt-3.5-turbo"; - input["config"] = config; - - runToCompletion([&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - auto last_msgs = mock_provider_->lastMessages(); - EXPECT_EQ(last_msgs[0].content, "Custom system prompt"); - EXPECT_EQ(mock_provider_->lastConfig().model, "gpt-3.5-turbo"); -} - -TEST_F(AgentRunnableTest, MaxIterations) { - // Set up agent to always call tools (never complete) - for (int i = 0; i < 15; ++i) { - std::vector calls; - JsonValue args = JsonValue::object(); - args["query"] = "test"; - calls.push_back(ToolCall("call_" + std::to_string(i), "search", args)); - mock_provider_->queueToolCalls(calls); - } - - // Create agent with low max iterations - auto limited_agent = AgentRunnable::create( - mock_provider_, executor_, AgentConfig("gpt-4").withMaxIterations(3)); - - JsonValue input = "Test query"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - limited_agent->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["status"].getString(), "max_iterations_reached"); - EXPECT_EQ(result["iterations"].getInt(), 3); -} - -// ============================================================================= -// Context Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, WithContext) { - mock_provider_->setDefaultResponse("I remember you asked about weather."); - - JsonValue input = JsonValue::object(); - input["query"] = "What did I ask before?"; - - JsonValue context = JsonValue::array(); - JsonValue msg1 = JsonValue::object(); - msg1["role"] = "user"; - msg1["content"] = "What is the weather?"; - context.push_back(msg1); - - JsonValue msg2 = JsonValue::object(); - msg2["role"] = "assistant"; - msg2["content"] = "The weather is sunny."; - context.push_back(msg2); - - input["context"] = context; - - runToCompletion([&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // Verify context was included - auto last_msgs = mock_provider_->lastMessages(); - EXPECT_GE(last_msgs.size(), 4u); // system + 2 context + query - EXPECT_EQ(last_msgs[1].content, "What is the weather?"); - EXPECT_EQ(last_msgs[2].content, "The weather is sunny."); -} - -TEST_F(AgentRunnableTest, LangGraphStyleInput) { - mock_provider_->setDefaultResponse("I understand."); - - JsonValue input = JsonValue::object(); - JsonValue messages = JsonValue::array(); - - JsonValue msg = JsonValue::object(); - msg["role"] = "user"; - msg["content"] = "Hello from messages array"; - messages.push_back(msg); - - input["messages"] = messages; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["status"].getString(), "completed"); - - // Verify message was used - auto last_msgs = mock_provider_->lastMessages(); - bool found = false; - for (const auto& m : last_msgs) { - if (m.content == "Hello from messages array") { - found = true; - break; - } - } - EXPECT_TRUE(found); -} - -// ============================================================================= -// Callback Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, StepCallback) { - // First call: tool call - std::vector calls; - JsonValue args = JsonValue::object(); - args["query"] = "test"; - calls.push_back(ToolCall("call_1", "search", args)); - mock_provider_->queueToolCalls(calls); - - // Second call: final response - mock_provider_->queueResponse("Done!"); - - std::vector recorded_steps; - agent_->setStepCallback([&recorded_steps](const AgentStep& step) { - recorded_steps.push_back(step); - }); - - JsonValue input = "Test"; - - runToCompletion([&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(recorded_steps.size(), 2u); - EXPECT_EQ(recorded_steps[0].step_number, 1); - EXPECT_EQ(recorded_steps[1].step_number, 2); -} - -TEST_F(AgentRunnableTest, ToolApprovalCallback) { - std::vector calls; - JsonValue args = JsonValue::object(); - args["query"] = "test"; - calls.push_back(ToolCall("call_1", "search", args)); - mock_provider_->queueToolCalls(calls); - - // Reject all tool calls - agent_->setToolApprovalCallback([](const ToolCall& call) { - return false; // Reject - }); - - JsonValue input = "Test"; - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, AgentError::CANCELLED); -} - -// ============================================================================= -// Error Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, NoProviderError) { - auto agent_no_provider = AgentRunnable::create(nullptr, AgentConfig("gpt-4")); - - JsonValue input = "Test"; - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - agent_no_provider->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, AgentError::NO_PROVIDER); -} - -TEST_F(AgentRunnableTest, EmptyInput) { - JsonValue input = JsonValue::object(); - // No query or messages - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); -} - -TEST_F(AgentRunnableTest, LLMError) { - mock_provider_->queueError(LLMError::RATE_LIMITED, "Rate limit exceeded"); - - JsonValue input = "Test"; - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, LLMError::RATE_LIMITED); -} - -TEST_F(AgentRunnableTest, AgentWithoutTools) { - // Create agent without tools - auto agent_no_tools = AgentRunnable::create( - mock_provider_, - AgentConfig("gpt-4").withSystemPrompt("You are helpful.")); - - // LLM tries to call a tool anyway - std::vector calls; - JsonValue args = JsonValue::object(); - calls.push_back(ToolCall("call_1", "search", args)); - mock_provider_->queueToolCalls(calls); - - // LLM handles the error gracefully - mock_provider_->queueResponse("I cannot search, but I can help otherwise."); - - JsonValue input = "Search for something"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_no_tools->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["status"].getString(), "completed"); -} - -// ============================================================================= -// Output Structure Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, OutputContainsUsage) { - LLMResponse response; - response.message = Message::assistant("Test response"); - response.finish_reason = "stop"; - response.usage = Usage(100, 50); - mock_provider_->queueFullResponse(response); - - JsonValue input = "Test"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result.contains("usage")); - EXPECT_EQ(result["usage"]["prompt_tokens"].getInt(), 100); - EXPECT_EQ(result["usage"]["completion_tokens"].getInt(), 50); - EXPECT_EQ(result["usage"]["total_tokens"].getInt(), 150); -} - -TEST_F(AgentRunnableTest, OutputContainsDuration) { - mock_provider_->setDefaultResponse("Quick response"); - - JsonValue input = "Test"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result.contains("duration_ms")); - EXPECT_GE(result["duration_ms"].getInt(), 0); -} - -// ============================================================================= -// Factory Function Tests -// ============================================================================= - -TEST_F(AgentRunnableTest, MakeAgentRunnableWithRegistry) { - auto agent = - makeAgentRunnable(mock_provider_, registry_, AgentConfig("gpt-4")); - EXPECT_NE(agent, nullptr); - EXPECT_EQ(agent->provider(), mock_provider_); - EXPECT_EQ(agent->registry(), registry_); -} - -TEST_F(AgentRunnableTest, MakeAgentRunnableWithoutTools) { - auto agent = makeAgentRunnable(mock_provider_, AgentConfig("gpt-4")); - EXPECT_NE(agent, nullptr); - EXPECT_EQ(agent->provider(), mock_provider_); - EXPECT_EQ(agent->registry(), nullptr); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/agent_state_test.cc b/third_party/gopher-orch/tests/gopher/orch/agent_state_test.cc deleted file mode 100644 index 289354d5..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/agent_state_test.cc +++ /dev/null @@ -1,392 +0,0 @@ -// Unit tests for AgentState reducer and JSON serialization - -#include "gopher/orch/agent/agent_types.h" -#include "gtest/gtest.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -// ============================================================================= -// AgentState Reducer Tests -// ============================================================================= - -TEST(AgentStateReducerTest, MessagesAppend) { - AgentState current; - current.messages.push_back(Message::user("Hello")); - current.messages.push_back(Message::assistant("Hi there!")); - - AgentState update; - update.messages.push_back(Message::user("How are you?")); - - auto result = AgentState::reduce(current, update); - - EXPECT_EQ(result.messages.size(), 3u); - EXPECT_EQ(result.messages[0].content, "Hello"); - EXPECT_EQ(result.messages[1].content, "Hi there!"); - EXPECT_EQ(result.messages[2].content, "How are you?"); -} - -TEST(AgentStateReducerTest, StepsAppend) { - AgentState current; - AgentStep step1; - step1.step_number = 1; - step1.llm_message = Message::assistant("First response"); - current.steps.push_back(step1); - - AgentState update; - AgentStep step2; - step2.step_number = 2; - step2.llm_message = Message::assistant("Second response"); - update.steps.push_back(step2); - - auto result = AgentState::reduce(current, update); - - EXPECT_EQ(result.steps.size(), 2u); - EXPECT_EQ(result.steps[0].step_number, 1); - EXPECT_EQ(result.steps[1].step_number, 2); -} - -TEST(AgentStateReducerTest, UsageAccumulates) { - AgentState current; - current.total_usage.prompt_tokens = 100; - current.total_usage.completion_tokens = 50; - current.total_usage.total_tokens = 150; - - AgentState update; - update.total_usage.prompt_tokens = 80; - update.total_usage.completion_tokens = 30; - update.total_usage.total_tokens = 110; - - auto result = AgentState::reduce(current, update); - - EXPECT_EQ(result.total_usage.prompt_tokens, 180); - EXPECT_EQ(result.total_usage.completion_tokens, 80); - EXPECT_EQ(result.total_usage.total_tokens, 260); -} - -TEST(AgentStateReducerTest, StatusLastWriteWins) { - AgentState current; - current.status = AgentStatus::RUNNING; - - AgentState update; - update.status = AgentStatus::COMPLETED; - - auto result = AgentState::reduce(current, update); - - EXPECT_EQ(result.status, AgentStatus::COMPLETED); -} - -TEST(AgentStateReducerTest, IterationCountsLastWriteWins) { - AgentState current; - current.current_iteration = 2; - current.remaining_steps = 8; - - AgentState update; - update.current_iteration = 3; - update.remaining_steps = 7; - - auto result = AgentState::reduce(current, update); - - EXPECT_EQ(result.current_iteration, 3); - EXPECT_EQ(result.remaining_steps, 7); -} - -TEST(AgentStateReducerTest, ErrorLastWriteWins) { - AgentState current; - current.error = Error(-1, "First error"); - - AgentState update; - update.error = Error(-2, "Second error"); - - auto result = AgentState::reduce(current, update); - - EXPECT_TRUE(result.error.has_value()); - EXPECT_EQ(result.error->code, -2); - EXPECT_EQ(result.error->message, "Second error"); -} - -TEST(AgentStateReducerTest, ClearError) { - AgentState current; - current.error = Error(-1, "Had error"); - - AgentState update; - // update.error is nullopt - - auto result = AgentState::reduce(current, update); - - EXPECT_FALSE(result.error.has_value()); -} - -TEST(AgentStateReducerTest, EmptyStates) { - AgentState current; - AgentState update; - - auto result = AgentState::reduce(current, update); - - EXPECT_TRUE(result.messages.empty()); - EXPECT_TRUE(result.steps.empty()); - EXPECT_EQ(result.status, AgentStatus::IDLE); -} - -// ============================================================================= -// AgentState JSON Serialization Tests -// ============================================================================= - -TEST(AgentStateJsonTest, ToJsonBasic) { - AgentState state; - state.status = AgentStatus::RUNNING; - state.current_iteration = 2; - state.remaining_steps = 8; - state.messages.push_back(Message::user("Hello")); - state.messages.push_back(Message::assistant("Hi!")); - state.total_usage = Usage(100, 50); - - JsonValue json = state.toJson(); - - EXPECT_TRUE(json.isObject()); - EXPECT_EQ(json["status"].getString(), "running"); - EXPECT_EQ(json["current_iteration"].getInt(), 2); - EXPECT_EQ(json["remaining_steps"].getInt(), 8); - EXPECT_TRUE(json["messages"].isArray()); - EXPECT_EQ(json["messages"].size(), 2u); - EXPECT_EQ(json["messages"][0]["role"].getString(), "user"); - EXPECT_EQ(json["messages"][0]["content"].getString(), "Hello"); - EXPECT_EQ(json["messages"][1]["role"].getString(), "assistant"); - EXPECT_EQ(json["usage"]["prompt_tokens"].getInt(), 100); - EXPECT_EQ(json["usage"]["completion_tokens"].getInt(), 50); - EXPECT_EQ(json["usage"]["total_tokens"].getInt(), 150); -} - -TEST(AgentStateJsonTest, ToJsonWithToolCalls) { - AgentState state; - state.status = AgentStatus::RUNNING; - - std::vector calls; - JsonValue args = JsonValue::object(); - args["query"] = "test"; - calls.push_back(ToolCall("call_1", "search", args)); - state.messages.push_back(Message::assistantWithToolCalls(calls)); - - JsonValue json = state.toJson(); - - auto& msg = json["messages"][0]; - EXPECT_TRUE(msg.contains("tool_calls")); - EXPECT_TRUE(msg["tool_calls"].isArray()); - EXPECT_EQ(msg["tool_calls"].size(), 1u); - EXPECT_EQ(msg["tool_calls"][0]["id"].getString(), "call_1"); - EXPECT_EQ(msg["tool_calls"][0]["name"].getString(), "search"); - EXPECT_EQ(msg["tool_calls"][0]["arguments"]["query"].getString(), "test"); -} - -TEST(AgentStateJsonTest, ToJsonWithToolResult) { - AgentState state; - state.messages.push_back(Message::toolResult("call_1", "Result data")); - - JsonValue json = state.toJson(); - - auto& msg = json["messages"][0]; - EXPECT_EQ(msg["role"].getString(), "tool"); - EXPECT_EQ(msg["content"].getString(), "Result data"); - EXPECT_EQ(msg["tool_call_id"].getString(), "call_1"); -} - -TEST(AgentStateJsonTest, ToJsonWithError) { - AgentState state; - state.status = AgentStatus::FAILED; - state.error = Error(-1, "Something went wrong"); - - JsonValue json = state.toJson(); - - EXPECT_TRUE(json.contains("error")); - EXPECT_EQ(json["error"]["code"].getInt(), -1); - EXPECT_EQ(json["error"]["message"].getString(), "Something went wrong"); -} - -TEST(AgentStateJsonTest, FromJsonBasic) { - JsonValue json = JsonValue::object(); - json["status"] = "completed"; - json["current_iteration"] = 3; - json["remaining_steps"] = 7; - - JsonValue messages = JsonValue::array(); - JsonValue msg1 = JsonValue::object(); - msg1["role"] = "user"; - msg1["content"] = "Hello"; - messages.push_back(msg1); - - JsonValue msg2 = JsonValue::object(); - msg2["role"] = "assistant"; - msg2["content"] = "Hi there!"; - messages.push_back(msg2); - - json["messages"] = messages; - - JsonValue usage = JsonValue::object(); - usage["prompt_tokens"] = 100; - usage["completion_tokens"] = 50; - usage["total_tokens"] = 150; - json["usage"] = usage; - - AgentState state = AgentState::fromJson(json); - - EXPECT_EQ(state.status, AgentStatus::COMPLETED); - EXPECT_EQ(state.current_iteration, 3); - EXPECT_EQ(state.remaining_steps, 7); - EXPECT_EQ(state.messages.size(), 2u); - EXPECT_EQ(state.messages[0].role, Role::USER); - EXPECT_EQ(state.messages[0].content, "Hello"); - EXPECT_EQ(state.messages[1].role, Role::ASSISTANT); - EXPECT_EQ(state.total_usage.prompt_tokens, 100); - EXPECT_EQ(state.total_usage.completion_tokens, 50); -} - -TEST(AgentStateJsonTest, FromJsonWithToolCalls) { - JsonValue json = JsonValue::object(); - json["status"] = "running"; - - JsonValue messages = JsonValue::array(); - JsonValue msg = JsonValue::object(); - msg["role"] = "assistant"; - msg["content"] = ""; - - JsonValue tool_calls = JsonValue::array(); - JsonValue call = JsonValue::object(); - call["id"] = "call_123"; - call["name"] = "search"; - JsonValue args = JsonValue::object(); - args["query"] = "weather"; - call["arguments"] = args; - tool_calls.push_back(call); - msg["tool_calls"] = tool_calls; - - messages.push_back(msg); - json["messages"] = messages; - - AgentState state = AgentState::fromJson(json); - - EXPECT_EQ(state.messages.size(), 1u); - EXPECT_TRUE(state.messages[0].hasToolCalls()); - EXPECT_EQ(state.messages[0].tool_calls->size(), 1u); - EXPECT_EQ((*state.messages[0].tool_calls)[0].id, "call_123"); - EXPECT_EQ((*state.messages[0].tool_calls)[0].name, "search"); -} - -TEST(AgentStateJsonTest, FromJsonWithError) { - JsonValue json = JsonValue::object(); - json["status"] = "failed"; - - JsonValue error = JsonValue::object(); - error["code"] = -100; - error["message"] = "Rate limited"; - json["error"] = error; - - AgentState state = AgentState::fromJson(json); - - EXPECT_EQ(state.status, AgentStatus::FAILED); - EXPECT_TRUE(state.error.has_value()); - EXPECT_EQ(state.error->code, -100); - EXPECT_EQ(state.error->message, "Rate limited"); -} - -TEST(AgentStateJsonTest, RoundTrip) { - // Create a complex state - AgentState original; - original.status = AgentStatus::RUNNING; - original.current_iteration = 2; - original.remaining_steps = 8; - original.total_usage = Usage(150, 75); - - original.messages.push_back(Message::system("You are helpful")); - original.messages.push_back(Message::user("Search for weather")); - - std::vector calls; - JsonValue args = JsonValue::object(); - args["query"] = "weather tokyo"; - calls.push_back(ToolCall("call_1", "search", args)); - original.messages.push_back(Message::assistantWithToolCalls(calls)); - - original.messages.push_back(Message::toolResult("call_1", "Sunny, 25C")); - original.messages.push_back(Message::assistant("The weather is sunny.")); - - // Convert to JSON and back - JsonValue json = original.toJson(); - AgentState restored = AgentState::fromJson(json); - - // Verify - EXPECT_EQ(restored.status, original.status); - EXPECT_EQ(restored.current_iteration, original.current_iteration); - EXPECT_EQ(restored.remaining_steps, original.remaining_steps); - EXPECT_EQ(restored.total_usage.prompt_tokens, - original.total_usage.prompt_tokens); - EXPECT_EQ(restored.messages.size(), original.messages.size()); - - // Check messages - EXPECT_EQ(restored.messages[0].role, Role::SYSTEM); - EXPECT_EQ(restored.messages[1].role, Role::USER); - EXPECT_EQ(restored.messages[2].role, Role::ASSISTANT); - EXPECT_TRUE(restored.messages[2].hasToolCalls()); - EXPECT_EQ(restored.messages[3].role, Role::TOOL); - EXPECT_EQ(*restored.messages[3].tool_call_id, "call_1"); - EXPECT_EQ(restored.messages[4].content, "The weather is sunny."); -} - -TEST(AgentStateJsonTest, FromJsonInvalid) { - // Non-object input should return default state - JsonValue json = JsonValue::array(); - AgentState state = AgentState::fromJson(json); - - EXPECT_EQ(state.status, AgentStatus::IDLE); - EXPECT_TRUE(state.messages.empty()); -} - -// ============================================================================= -// AgentState Helper Method Tests -// ============================================================================= - -TEST(AgentStateTest, IsRunning) { - AgentState state; - EXPECT_FALSE(state.isRunning()); - - state.status = AgentStatus::RUNNING; - EXPECT_TRUE(state.isRunning()); - - state.status = AgentStatus::COMPLETED; - EXPECT_FALSE(state.isRunning()); -} - -TEST(AgentStateTest, IsCompleted) { - AgentState state; - EXPECT_FALSE(state.isCompleted()); - - state.status = AgentStatus::COMPLETED; - EXPECT_TRUE(state.isCompleted()); - - state.status = AgentStatus::FAILED; - EXPECT_FALSE(state.isCompleted()); -} - -TEST(AgentStateTest, LastContent) { - AgentState state; - EXPECT_EQ(state.lastContent(), ""); - - state.messages.push_back(Message::user("First")); - EXPECT_EQ(state.lastContent(), "First"); - - state.messages.push_back(Message::assistant("Second")); - EXPECT_EQ(state.lastContent(), "Second"); -} - -// ============================================================================= -// AgentStatus Tests -// ============================================================================= - -TEST(AgentStatusTest, ToString) { - EXPECT_EQ(agentStatusToString(AgentStatus::IDLE), "idle"); - EXPECT_EQ(agentStatusToString(AgentStatus::RUNNING), "running"); - EXPECT_EQ(agentStatusToString(AgentStatus::COMPLETED), "completed"); - EXPECT_EQ(agentStatusToString(AgentStatus::FAILED), "failed"); - EXPECT_EQ(agentStatusToString(AgentStatus::CANCELLED), "cancelled"); - EXPECT_EQ(agentStatusToString(AgentStatus::MAX_ITERATIONS_REACHED), - "max_iterations_reached"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/agent_test.cc b/third_party/gopher-orch/tests/gopher/orch/agent_test.cc deleted file mode 100644 index 9123f752..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/agent_test.cc +++ /dev/null @@ -1,462 +0,0 @@ -// Unit tests for ReActAgent - -#include "gopher/orch/agent/agent.h" - -#include "gopher/orch/agent/agent_types.h" -#include "gopher/orch/agent/tool_registry.h" -#include "mock_llm_provider.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; - -// ============================================================================= -// Agent Test Fixture -// ============================================================================= - -class AgentTest : public OrchTest { - protected: - std::shared_ptr provider_; - ToolRegistryPtr registry_; - - void SetUp() override { - OrchTest::SetUp(); - provider_ = makeMockLLMProvider("test-provider"); - registry_ = makeToolRegistry(); - } - - // Helper to run agent to completion - AgentResult runAgent(ReActAgent::Ptr agent, const std::string& query) { - return runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent->run(query, d, std::move(cb)); - }); - } - - // Helper to run agent and allow errors - Result runAgentResult(ReActAgent::Ptr agent, - const std::string& query) { - return runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - agent->run(query, d, std::move(cb)); - }); - } -}; - -// ============================================================================= -// Basic Agent Tests -// ============================================================================= - -TEST_F(AgentTest, CreateAgent) { - auto agent = ReActAgent::create(provider_, registry_); - EXPECT_NE(agent, nullptr); - EXPECT_FALSE(agent->isRunning()); - EXPECT_EQ(agent->provider(), provider_); - EXPECT_EQ(agent->tools(), registry_); -} - -TEST_F(AgentTest, CreateAgentWithConfig) { - AgentConfig config("gpt-4"); - config.withSystemPrompt("You are a helpful assistant.") - .withMaxIterations(5) - .withTemperature(0.7); - - auto agent = ReActAgent::create(provider_, registry_, config); - - EXPECT_EQ(agent->config().llm_config.model, "gpt-4"); - EXPECT_EQ(agent->config().system_prompt, "You are a helpful assistant."); - EXPECT_EQ(agent->config().max_iterations, 5); - EXPECT_TRUE(agent->config().llm_config.temperature.has_value()); - EXPECT_DOUBLE_EQ(*agent->config().llm_config.temperature, 0.7); -} - -TEST_F(AgentTest, SimpleQuery) { - provider_->setDefaultResponse("Hello! How can I help you today?"); - - AgentConfig config("test-model"); - auto agent = ReActAgent::create(provider_, registry_, config); - - auto result = runAgent(agent, "Hello"); - - EXPECT_TRUE(result.isSuccess()); - EXPECT_EQ(result.status, AgentStatus::COMPLETED); - EXPECT_EQ(result.response, "Hello! How can I help you today?"); - EXPECT_EQ(result.iterationCount(), 1); - EXPECT_EQ(provider_->callCount(), 1u); -} - -TEST_F(AgentTest, SystemPromptIncluded) { - provider_->setDefaultResponse("I am a test assistant."); - - AgentConfig config("test-model"); - config.withSystemPrompt("You are a test assistant."); - auto agent = ReActAgent::create(provider_, registry_, config); - - runAgent(agent, "Who are you?"); - - auto messages = provider_->lastMessages(); - ASSERT_GE(messages.size(), 2u); - EXPECT_EQ(messages[0].role, Role::SYSTEM); - EXPECT_EQ(messages[0].content, "You are a test assistant."); - EXPECT_EQ(messages[1].role, Role::USER); - EXPECT_EQ(messages[1].content, "Who are you?"); -} - -// ============================================================================= -// Tool Execution Tests -// ============================================================================= - -TEST_F(AgentTest, SingleToolCall) { - // First response: call search tool - ToolCall call1("call_1", "search", JsonValue::object()); - provider_->queueToolCalls({call1}); - - // Second response: final answer - provider_->queueResponse("The search found: example result."); - - // Add search tool to registry - JsonValue search_result = JsonValue::object(); - search_result["result"] = "example result"; - - registry_->addSyncTool( - "search", "Search the web", JsonValue::object(), - [search_result](const JsonValue& args) -> Result { - return Result(search_result); - }); - - auto agent = ReActAgent::create(provider_, registry_); - auto result = runAgent(agent, "Search for something"); - - EXPECT_TRUE(result.isSuccess()); - EXPECT_EQ(result.status, AgentStatus::COMPLETED); - EXPECT_EQ(result.response, "The search found: example result."); - EXPECT_EQ(result.iterationCount(), 2); // Tool call + final response - EXPECT_EQ(provider_->callCount(), 2u); -} - -TEST_F(AgentTest, MultipleToolCalls) { - // First response: call two tools - ToolCall call1("call_1", "get_weather", JsonValue::object()); - ToolCall call2("call_2", "get_time", JsonValue::object()); - provider_->queueToolCalls({call1, call2}); - - // Second response: final answer - provider_->queueResponse("It's sunny and 3pm."); - - // Add tools - registry_->addSyncTool("get_weather", "Get weather", JsonValue::object(), - [](const JsonValue& args) -> Result { - JsonValue result = JsonValue::object(); - result["weather"] = "sunny"; - return Result(result); - }); - - registry_->addSyncTool("get_time", "Get time", JsonValue::object(), - [](const JsonValue& args) -> Result { - JsonValue result = JsonValue::object(); - result["time"] = "3pm"; - return Result(result); - }); - - auto agent = ReActAgent::create(provider_, registry_); - auto result = runAgent(agent, "What's the weather and time?"); - - EXPECT_TRUE(result.isSuccess()); - EXPECT_EQ(result.iterationCount(), 2); - - // Check that both tools were called - EXPECT_GE(result.steps.size(), 1u); - if (!result.steps.empty()) { - EXPECT_EQ(result.steps[0].tool_executions.size(), 2u); - } -} - -TEST_F(AgentTest, ChainedToolCalls) { - // First response: call tool A - ToolCall call1("call_1", "tool_a", JsonValue::object()); - provider_->queueToolCalls({call1}); - - // Second response: call tool B - ToolCall call2("call_2", "tool_b", JsonValue::object()); - provider_->queueToolCalls({call2}); - - // Third response: final answer - provider_->queueResponse("Done with chained calls."); - - registry_->addSyncTool("tool_a", "Tool A", JsonValue::object(), - [](const JsonValue& args) -> Result { - return Result(JsonValue("A result")); - }); - - registry_->addSyncTool("tool_b", "Tool B", JsonValue::object(), - [](const JsonValue& args) -> Result { - return Result(JsonValue("B result")); - }); - - auto agent = ReActAgent::create(provider_, registry_); - auto result = runAgent(agent, "Run chained tools"); - - EXPECT_TRUE(result.isSuccess()); - EXPECT_EQ(result.iterationCount(), 3); -} - -TEST_F(AgentTest, ToolNotFound) { - // Call a tool that doesn't exist - ToolCall call1("call_1", "nonexistent_tool", JsonValue::object()); - provider_->queueToolCalls({call1}); - provider_->queueResponse("Tool error handled."); - - auto agent = ReActAgent::create(provider_, registry_); - auto result = runAgent(agent, "Call missing tool"); - - // Agent should still complete (tool error is passed to LLM) - EXPECT_TRUE(result.isSuccess()); - - // Check that tool result message contains error - bool found_error_message = false; - for (const auto& msg : result.messages) { - if (msg.role == Role::TOOL && - msg.content.find("not found") != std::string::npos) { - found_error_message = true; - break; - } - } - EXPECT_TRUE(found_error_message); -} - -TEST_F(AgentTest, ToolExecutionError) { - ToolCall call1("call_1", "failing_tool", JsonValue::object()); - provider_->queueToolCalls({call1}); - provider_->queueResponse("Handled the tool error."); - - registry_->addSyncTool( - "failing_tool", "Tool that fails", JsonValue::object(), - [](const JsonValue& args) -> Result { - return Result(Error(-1, "Tool execution failed")); - }); - - auto agent = ReActAgent::create(provider_, registry_); - auto result = runAgent(agent, "Call failing tool"); - - EXPECT_TRUE(result.isSuccess()); - - // Check that error was recorded - if (!result.steps.empty() && !result.steps[0].tool_executions.empty()) { - EXPECT_FALSE(result.steps[0].tool_executions[0].success); - } -} - -// ============================================================================= -// Max Iterations and Timeout Tests -// ============================================================================= - -TEST_F(AgentTest, MaxIterationsReached) { - // Always return tool calls (will never complete naturally) - ToolCall call("call_1", "loop_tool", JsonValue::object()); - provider_->setDefaultToolCalls({call}); - - registry_->addSyncTool("loop_tool", "Loop forever", JsonValue::object(), - [](const JsonValue& args) -> Result { - return Result(JsonValue("looping")); - }); - - AgentConfig config("test-model"); - config.withMaxIterations(3); - - auto agent = ReActAgent::create(provider_, registry_, config); - auto result = runAgentResult(agent, "Loop forever"); - - EXPECT_TRUE(mcp::holds_alternative(result)); - auto error = mcp::get(result); - EXPECT_EQ(error.code, AgentError::MAX_ITERATIONS); -} - -// ============================================================================= -// Callback Tests -// ============================================================================= - -TEST_F(AgentTest, StepCallback) { - provider_->queueResponse("Step 1"); - provider_->queueResponse("Step 2"); - - // First call returns tool, second returns final response - ToolCall call1("call_1", "test_tool", JsonValue::object()); - provider_->reset(); // Clear queue - provider_->queueToolCalls({call1}); - provider_->queueResponse("Final answer"); - - registry_->addSyncTool("test_tool", "Test", JsonValue::object(), - [](const JsonValue& args) -> Result { - return Result(JsonValue("result")); - }); - - std::vector step_numbers; - - auto agent = ReActAgent::create(provider_, registry_); - agent->setStepCallback([&step_numbers](const AgentStep& step) { - step_numbers.push_back(step.step_number); - }); - - runAgent(agent, "Test with steps"); - - EXPECT_GE(step_numbers.size(), 1u); - if (!step_numbers.empty()) { - EXPECT_EQ(step_numbers[0], 1); - } -} - -TEST_F(AgentTest, ToolApprovalCallback) { - ToolCall call1("call_1", "approved_tool", JsonValue::object()); - ToolCall call2("call_2", "rejected_tool", JsonValue::object()); - provider_->queueToolCalls({call1, call2}); - - registry_->addSyncTool("approved_tool", "Approved", JsonValue::object(), - [](const JsonValue& args) -> Result { - return Result(JsonValue("approved")); - }); - - registry_->addSyncTool("rejected_tool", "Rejected", JsonValue::object(), - [](const JsonValue& args) -> Result { - return Result(JsonValue("rejected")); - }); - - auto agent = ReActAgent::create(provider_, registry_); - agent->setToolApprovalCallback([](const ToolCall& call) { - // Reject the "rejected_tool" - return call.name != "rejected_tool"; - }); - - auto result = runAgentResult(agent, "Call both tools"); - - // Agent should be cancelled due to rejected tool - EXPECT_TRUE(mcp::holds_alternative(result)); - auto error = mcp::get(result); - EXPECT_EQ(error.code, AgentError::CANCELLED); -} - -// ============================================================================= -// Context Tests -// ============================================================================= - -TEST_F(AgentTest, RunWithContext) { - provider_->setDefaultResponse("I remember the context."); - - std::vector context = {Message::user("My name is Alice"), - Message::assistant("Hello Alice!")}; - - auto agent = ReActAgent::create(provider_, registry_); - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - agent->run("What's my name?", context, d, std::move(cb)); - }); - - EXPECT_TRUE(result.isSuccess()); - - // Check that context was included - auto messages = provider_->lastMessages(); - ASSERT_GE(messages.size(), 3u); - EXPECT_EQ(messages[0].content, "My name is Alice"); - EXPECT_EQ(messages[1].content, "Hello Alice!"); - EXPECT_EQ(messages[2].content, "What's my name?"); -} - -// ============================================================================= -// State Tests -// ============================================================================= - -TEST_F(AgentTest, StateTracking) { - provider_->setDefaultResponse("Done"); - - auto agent = ReActAgent::create(provider_, registry_); - - EXPECT_EQ(agent->state().status, AgentStatus::IDLE); - EXPECT_FALSE(agent->isRunning()); - - runAgent(agent, "Test"); - - // After completion - EXPECT_EQ(agent->state().status, AgentStatus::COMPLETED); - EXPECT_FALSE(agent->isRunning()); - EXPECT_GE(agent->state().current_iteration, 1); -} - -TEST_F(AgentTest, UsageTracking) { - LLMResponse response; - response.message = Message::assistant("Response with usage"); - response.finish_reason = "stop"; - response.usage = Usage(100, 50); - - provider_->queueFullResponse(response); - - auto agent = ReActAgent::create(provider_, registry_); - auto result = runAgent(agent, "Test"); - - EXPECT_EQ(result.total_usage.prompt_tokens, 100); - EXPECT_EQ(result.total_usage.completion_tokens, 50); - EXPECT_EQ(result.total_usage.total_tokens, 150); -} - -// ============================================================================= -// Agent Types Tests -// ============================================================================= - -TEST(AgentTypesTest, AgentStatusToString) { - EXPECT_EQ(agentStatusToString(AgentStatus::IDLE), "idle"); - EXPECT_EQ(agentStatusToString(AgentStatus::RUNNING), "running"); - EXPECT_EQ(agentStatusToString(AgentStatus::COMPLETED), "completed"); - EXPECT_EQ(agentStatusToString(AgentStatus::FAILED), "failed"); - EXPECT_EQ(agentStatusToString(AgentStatus::CANCELLED), "cancelled"); - EXPECT_EQ(agentStatusToString(AgentStatus::MAX_ITERATIONS_REACHED), - "max_iterations_reached"); -} - -TEST(AgentTypesTest, AgentConfigBuilder) { - AgentConfig config("gpt-4"); - config.withSystemPrompt("System prompt") - .withMaxIterations(20) - .withTemperature(0.5) - .withMaxTokens(4000) - .withTimeout(std::chrono::milliseconds(60000)) - .withParallelToolCalls(false); - - EXPECT_EQ(config.llm_config.model, "gpt-4"); - EXPECT_EQ(config.system_prompt, "System prompt"); - EXPECT_EQ(config.max_iterations, 20); - EXPECT_TRUE(config.llm_config.temperature.has_value()); - EXPECT_DOUBLE_EQ(*config.llm_config.temperature, 0.5); - EXPECT_TRUE(config.llm_config.max_tokens.has_value()); - EXPECT_EQ(*config.llm_config.max_tokens, 4000); - EXPECT_EQ(config.timeout, std::chrono::milliseconds(60000)); - EXPECT_FALSE(config.parallel_tool_calls); -} - -TEST(AgentTypesTest, AgentState) { - AgentState state; - state.status = AgentStatus::RUNNING; - state.messages.push_back(Message::user("Hello")); - state.messages.push_back(Message::assistant("Hi!")); - - EXPECT_TRUE(state.isRunning()); - EXPECT_FALSE(state.isCompleted()); - EXPECT_EQ(state.lastContent(), "Hi!"); - - state.status = AgentStatus::COMPLETED; - EXPECT_FALSE(state.isRunning()); - EXPECT_TRUE(state.isCompleted()); -} - -TEST(AgentTypesTest, AgentResult) { - AgentResult result; - result.status = AgentStatus::COMPLETED; - result.response = "Final answer"; - result.steps.push_back(AgentStep()); - result.steps.push_back(AgentStep()); - result.total_usage = Usage(500, 200); - result.duration = std::chrono::milliseconds(1500); - - EXPECT_TRUE(result.isSuccess()); - EXPECT_EQ(result.iterationCount(), 2); - EXPECT_EQ(result.total_usage.total_tokens, 700); - EXPECT_EQ(result.duration.count(), 1500); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/callback_manager_test.cc b/third_party/gopher-orch/tests/gopher/orch/callback_manager_test.cc deleted file mode 100644 index c026cd09..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/callback_manager_test.cc +++ /dev/null @@ -1,499 +0,0 @@ -// Unit tests for CallbackManager and CallbackHandler - -#include "orch_test_fixture.h" - -using namespace gopher::orch::callback; - -// ============================================================================= -// Test Helper: Recording callback handler -// ============================================================================= - -class RecordingHandler : public CallbackHandler { - public: - struct ChainEvent { - std::string type; // "start", "end", "error" - std::string name; - core::JsonValue data; - }; - - struct ToolEvent { - std::string type; - std::string tool_name; - core::JsonValue data; - }; - - std::vector chain_events; - std::vector tool_events; - std::vector> custom_events; - std::mutex mutex; - - void onChainStart(const RunInfo& info, - const core::JsonValue& input) override { - std::lock_guard lock(mutex); - chain_events.push_back({"start", info.name, input}); - } - - void onChainEnd(const RunInfo& info, const core::JsonValue& output) override { - std::lock_guard lock(mutex); - chain_events.push_back({"end", info.name, output}); - } - - void onChainError(const RunInfo& info, const core::Error& error) override { - std::lock_guard lock(mutex); - core::JsonValue data = core::JsonValue::object(); - data["code"] = error.code; - data["message"] = error.message; - chain_events.push_back({"error", info.name, data}); - } - - void onToolStart(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& input) override { - std::lock_guard lock(mutex); - (void)info; - tool_events.push_back({"start", tool_name, input}); - } - - void onToolEnd(const RunInfo& info, - const std::string& tool_name, - const core::JsonValue& output) override { - std::lock_guard lock(mutex); - (void)info; - tool_events.push_back({"end", tool_name, output}); - } - - void onToolError(const RunInfo& info, - const std::string& tool_name, - const core::Error& error) override { - std::lock_guard lock(mutex); - (void)info; - core::JsonValue data = core::JsonValue::object(); - data["code"] = error.code; - data["message"] = error.message; - tool_events.push_back({"error", tool_name, data}); - } - - void onCustomEvent(const std::string& event_name, - const core::JsonValue& data) override { - std::lock_guard lock(mutex); - custom_events.push_back({event_name, data}); - } -}; - -// ============================================================================= -// CallbackHandler Tests -// ============================================================================= - -TEST_F(OrchTest, CallbackHandlerDefaultMethods) { - // Default handler should not crash when methods are called - CallbackHandler handler; - RunInfo info; - info.name = "test"; - core::JsonValue data = core::JsonValue::object(); - core::Error error(1, "test error"); - - // These should all be no-ops - handler.onChainStart(info, data); - handler.onChainEnd(info, data); - handler.onChainError(info, error); - handler.onToolStart(info, "tool", data); - handler.onToolEnd(info, "tool", data); - handler.onToolError(info, "tool", error); - handler.onCustomEvent("event", data); - handler.onRetry(info, error, 1, 3); -} - -TEST_F(OrchTest, NoOpCallbackHandler) { - NoOpCallbackHandler handler; - RunInfo info; - core::JsonValue data = core::JsonValue::object(); - core::Error error(1, "test error"); - - // Should compile and run without issues - handler.onChainStart(info, data); - handler.onChainEnd(info, data); - handler.onChainError(info, error); -} - -// ============================================================================= -// CallbackManager Tests -// ============================================================================= - -TEST_F(OrchTest, CallbackManagerBasic) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - EXPECT_EQ(manager->handlerCount(), 1u); - - // Emit chain events - core::JsonValue input = core::JsonValue::object(); - input["key"] = "value"; - - auto run_info = manager->startChain("test_chain", input); - EXPECT_FALSE(run_info.run_id.empty()); - EXPECT_EQ(run_info.name, "test_chain"); - EXPECT_EQ(run_info.run_type, "chain"); - - core::JsonValue output = core::JsonValue::object(); - output["result"] = "success"; - manager->endChain(run_info, output); - - // Verify events were recorded - EXPECT_EQ(handler->chain_events.size(), 2u); - EXPECT_EQ(handler->chain_events[0].type, "start"); - EXPECT_EQ(handler->chain_events[0].name, "test_chain"); - EXPECT_EQ(handler->chain_events[1].type, "end"); - EXPECT_EQ(handler->chain_events[1].name, "test_chain"); -} - -TEST_F(OrchTest, CallbackManagerChainError) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - core::JsonValue input = core::JsonValue::object(); - auto run_info = manager->startChain("failing_chain", input); - - core::Error error(OrchError::INTERNAL_ERROR, "Something went wrong"); - manager->errorChain(run_info, error); - - EXPECT_EQ(handler->chain_events.size(), 2u); - EXPECT_EQ(handler->chain_events[0].type, "start"); - EXPECT_EQ(handler->chain_events[1].type, "error"); - EXPECT_EQ(handler->chain_events[1].data["code"].getInt(), - OrchError::INTERNAL_ERROR); -} - -TEST_F(OrchTest, CallbackManagerToolEvents) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - core::JsonValue input = core::JsonValue::object(); - input["arg"] = "test"; - - auto run_info = manager->startTool("my_tool", input); - EXPECT_EQ(run_info.run_type, "tool"); - - core::JsonValue output = core::JsonValue::object(); - output["result"] = 42; - manager->endTool(run_info, "my_tool", output); - - EXPECT_EQ(handler->tool_events.size(), 2u); - EXPECT_EQ(handler->tool_events[0].type, "start"); - EXPECT_EQ(handler->tool_events[0].tool_name, "my_tool"); - EXPECT_EQ(handler->tool_events[1].type, "end"); - EXPECT_EQ(handler->tool_events[1].tool_name, "my_tool"); -} - -TEST_F(OrchTest, CallbackManagerCustomEvents) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - core::JsonValue data = core::JsonValue::object(); - data["fsm"] = "connection"; - data["from"] = "disconnected"; - data["to"] = "connecting"; - - manager->emitCustomEvent("fsm.transition", data); - - EXPECT_EQ(handler->custom_events.size(), 1u); - EXPECT_EQ(handler->custom_events[0].first, "fsm.transition"); - EXPECT_EQ(handler->custom_events[0].second["fsm"].getString(), "connection"); -} - -TEST_F(OrchTest, CallbackManagerMultipleHandlers) { - auto manager = std::make_shared(); - auto handler1 = std::make_shared(); - auto handler2 = std::make_shared(); - - manager->addHandler(handler1); - manager->addHandler(handler2); - EXPECT_EQ(manager->handlerCount(), 2u); - - core::JsonValue input = core::JsonValue::object(); - auto run_info = manager->startChain("multi_handler_chain", input); - manager->endChain(run_info, input); - - // Both handlers should have received the events - EXPECT_EQ(handler1->chain_events.size(), 2u); - EXPECT_EQ(handler2->chain_events.size(), 2u); -} - -TEST_F(OrchTest, CallbackManagerRemoveHandler) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - EXPECT_EQ(manager->handlerCount(), 1u); - - manager->removeHandler(handler); - EXPECT_EQ(manager->handlerCount(), 0u); - - // Events should not be received after removal - core::JsonValue input = core::JsonValue::object(); - auto run_info = manager->startChain("after_removal", input); - - EXPECT_EQ(handler->chain_events.size(), 0u); -} - -TEST_F(OrchTest, CallbackManagerClearHandlers) { - auto manager = std::make_shared(); - auto handler1 = std::make_shared(); - auto handler2 = std::make_shared(); - - manager->addHandler(handler1); - manager->addHandler(handler2); - EXPECT_EQ(manager->handlerCount(), 2u); - - manager->clearHandlers(); - EXPECT_EQ(manager->handlerCount(), 0u); -} - -TEST_F(OrchTest, CallbackManagerChildManager) { - auto parent = std::make_shared(); - auto handler = std::make_shared(); - - parent->addHandler(handler); - - // Create child manager - auto child = parent->child(); - - // Child should inherit handlers - EXPECT_EQ(child->handlerCount(), 1u); - - // Child should have parent_run_id set - EXPECT_EQ(child->parentRunId(), parent->runId()); - - // Events from child should be received - core::JsonValue input = core::JsonValue::object(); - auto run_info = child->startChain("child_chain", input); - - EXPECT_EQ(handler->chain_events.size(), 1u); - EXPECT_EQ(run_info.parent_run_id, parent->runId()); -} - -TEST_F(OrchTest, CallbackManagerTags) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - manager->addTags({"env:prod", "version:1.0"}); - - core::JsonValue input = core::JsonValue::object(); - auto run_info = manager->startChain("tagged_chain", input, {"extra:tag"}); - - // Should have both inheritable and provided tags - EXPECT_EQ(run_info.tags.size(), 3u); -} - -TEST_F(OrchTest, CallbackManagerMetadata) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - core::JsonValue user_id = core::JsonValue("user123"); - manager->addMetadata("user_id", user_id); - - core::JsonValue input = core::JsonValue::object(); - core::JsonValue extra_metadata = core::JsonValue::object(); - extra_metadata["request_id"] = "req456"; - - auto run_info = - manager->startChain("metadata_chain", input, {}, extra_metadata); - - // Should have merged metadata - EXPECT_EQ(run_info.metadata["user_id"].getString(), "user123"); - EXPECT_EQ(run_info.metadata["request_id"].getString(), "req456"); -} - -// ============================================================================= -// ChainGuard Tests -// ============================================================================= - -TEST_F(OrchTest, ChainGuardSuccess) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - { - core::JsonValue input = core::JsonValue::object(); - ChainGuard guard(manager, "guarded_chain", input); - - // Simulate work... - core::JsonValue output = core::JsonValue::object(); - output["status"] = "done"; - guard.setOutput(output); - } - - EXPECT_EQ(handler->chain_events.size(), 2u); - EXPECT_EQ(handler->chain_events[0].type, "start"); - EXPECT_EQ(handler->chain_events[1].type, "end"); -} - -TEST_F(OrchTest, ChainGuardError) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - { - core::JsonValue input = core::JsonValue::object(); - ChainGuard guard(manager, "failing_guarded_chain", input); - - core::Error error(OrchError::INTERNAL_ERROR, "Failed"); - guard.setError(error); - } - - EXPECT_EQ(handler->chain_events.size(), 2u); - EXPECT_EQ(handler->chain_events[0].type, "start"); - EXPECT_EQ(handler->chain_events[1].type, "error"); -} - -TEST_F(OrchTest, ChainGuardAutoError) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - { - core::JsonValue input = core::JsonValue::object(); - ChainGuard guard(manager, "unfinished_chain", input); - // Guard goes out of scope without setOutput/setError - } - - // Should automatically emit error - EXPECT_EQ(handler->chain_events.size(), 2u); - EXPECT_EQ(handler->chain_events[0].type, "start"); - EXPECT_EQ(handler->chain_events[1].type, "error"); -} - -// ============================================================================= -// ToolGuard Tests -// ============================================================================= - -TEST_F(OrchTest, ToolGuardSuccess) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - { - core::JsonValue input = core::JsonValue::object(); - ToolGuard guard(manager, "guarded_tool", input); - - core::JsonValue output = core::JsonValue::object(); - output["result"] = 42; - guard.setOutput(output); - } - - EXPECT_EQ(handler->tool_events.size(), 2u); - EXPECT_EQ(handler->tool_events[0].type, "start"); - EXPECT_EQ(handler->tool_events[1].type, "end"); -} - -TEST_F(OrchTest, ToolGuardAutoError) { - auto manager = std::make_shared(); - auto handler = std::make_shared(); - - manager->addHandler(handler); - - { - core::JsonValue input = core::JsonValue::object(); - ToolGuard guard(manager, "unfinished_tool", input); - // Guard goes out of scope without completion - } - - EXPECT_EQ(handler->tool_events.size(), 2u); - EXPECT_EQ(handler->tool_events[0].type, "start"); - EXPECT_EQ(handler->tool_events[1].type, "error"); -} - -// ============================================================================= -// RunInfo Tests -// ============================================================================= - -TEST_F(OrchTest, RunInfoDuration) { - RunInfo info; - info.start_time = std::chrono::steady_clock::now(); - - // Sleep a bit - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - auto duration = info.durationMs(); - EXPECT_GE(duration.count(), 10); -} - -// ============================================================================= -// LoggingCallbackHandler Tests -// ============================================================================= - -TEST_F(OrchTest, LoggingCallbackHandlerBasic) { - // Just verify it doesn't crash - LoggingCallbackHandler handler(LoggingCallbackHandler::LogLevel::DEBUG); - - RunInfo info; - info.name = "test"; - info.start_time = std::chrono::steady_clock::now(); - - core::JsonValue data = core::JsonValue::object(); - data["key"] = "value"; - - handler.onChainStart(info, data); - handler.onChainEnd(info, data); - handler.onChainError(info, core::Error(1, "test error")); - handler.onToolStart(info, "tool", data); - handler.onToolEnd(info, "tool", data); - handler.onToolError(info, "tool", core::Error(1, "test error")); - handler.onCustomEvent("custom", data); - handler.onRetry(info, core::Error(1, "retry error"), 1, 3); -} - -// ============================================================================= -// RunnableConfig Callbacks Integration Tests -// ============================================================================= - -TEST_F(OrchTest, RunnableConfigWithCallbacks) { - auto manager = std::make_shared(); - - RunnableConfig config; - config.withCallbacks(manager); - - EXPECT_TRUE(config.hasCallbacks()); - EXPECT_EQ(config.callbacks(), manager); -} - -TEST_F(OrchTest, RunnableConfigCallbacksInheritance) { - auto manager = std::make_shared(); - - RunnableConfig parent; - parent.withCallbacks(manager); - - RunnableConfig child = parent.child(); - - // Child should inherit callbacks - EXPECT_TRUE(child.hasCallbacks()); - EXPECT_EQ(child.callbacks(), manager); -} - -TEST_F(OrchTest, RunnableConfigMergeCallbacks) { - auto manager1 = std::make_shared(); - auto manager2 = std::make_shared(); - - RunnableConfig config1; - config1.withCallbacks(manager1); - - RunnableConfig config2; - config2.withCallbacks(manager2); - - config1.merge(config2); - - // Merged callbacks should be from config2 - EXPECT_EQ(config1.callbacks(), manager2); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/circuit_breaker_test.cc b/third_party/gopher-orch/tests/gopher/orch/circuit_breaker_test.cc deleted file mode 100644 index 40117486..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/circuit_breaker_test.cc +++ /dev/null @@ -1,96 +0,0 @@ -// Unit tests for CircuitBreaker resilience pattern - -#include "orch_test_fixture.h" - -// ============================================================================= -// CircuitBreaker Tests -// ============================================================================= - -TEST_F(OrchTest, CircuitBreakerClosed) { - // Normal operation - circuit stays closed - auto successLambda = makeJsonLambda( - [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["ok"] = JsonValue(true); - return makeSuccess(JsonValue(result)); - }, - "SuccessLambda"); - - auto cb = withCircuitBreaker(successLambda); - - EXPECT_EQ(cb->state(), CircuitState::CLOSED); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb_fn) { - cb->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb_fn)); - }); - - EXPECT_TRUE(result["ok"].getBool()); - EXPECT_EQ(cb->state(), CircuitState::CLOSED); -} - -TEST_F(OrchTest, CircuitBreakerOpens) { - // Circuit opens after threshold failures - std::atomic call_count{0}; - - auto failingLambda = makeJsonLambda( - [&call_count](const JsonValue&) -> Result { - call_count++; - return Result(Error(OrchError::INTERNAL_ERROR, "Failed")); - }, - "FailingLambda"); - - CircuitBreakerPolicy policy; - policy.failure_threshold = 3; - policy.recovery_timeout_ms = 60000; // Long timeout for test - auto cb = withCircuitBreaker(failingLambda, policy); - - EXPECT_EQ(cb->state(), CircuitState::CLOSED); - - // Cause failures to open circuit - for (int i = 0; i < 3; i++) { - auto result = runToCompletionResult([&](Dispatcher& d, - JsonCallback cb_fn) { - cb->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb_fn)); - }); - EXPECT_TRUE(mcp::holds_alternative(result)); - } - - EXPECT_EQ(cb->state(), CircuitState::OPEN); - EXPECT_EQ(call_count.load(), 3); - - // Next call should fail fast without calling inner - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb_fn) { - cb->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb_fn)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::CIRCUIT_OPEN); - EXPECT_EQ(call_count.load(), 3); // Inner not called -} - -TEST_F(OrchTest, CircuitBreakerReset) { - // Manual reset works - auto failingLambda = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result(Error(OrchError::INTERNAL_ERROR, "Failed")); - }, - "FailingLambda"); - - CircuitBreakerPolicy policy; - policy.failure_threshold = 1; // Open after 1 failure - auto cb = withCircuitBreaker(failingLambda, policy); - - // Cause failure to open circuit - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb_fn) { - cb->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb_fn)); - }); - - EXPECT_EQ(cb->state(), CircuitState::OPEN); - - // Reset should close circuit - cb->reset(); - EXPECT_EQ(cb->state(), CircuitState::CLOSED); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/fallback_test.cc b/third_party/gopher-orch/tests/gopher/orch/fallback_test.cc deleted file mode 100644 index 9f78bf31..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/fallback_test.cc +++ /dev/null @@ -1,102 +0,0 @@ -// Unit tests for Fallback resilience pattern - -#include "orch_test_fixture.h" - -// ============================================================================= -// Fallback Tests -// ============================================================================= - -TEST_F(OrchTest, FallbackPrimarySuccess) { - // Primary succeeds, fallback not used - std::atomic fallback_called{0}; - - auto primary = makeJsonLambda( - [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["source"] = JsonValue("primary"); - return makeSuccess(JsonValue(result)); - }, - "Primary"); - - auto fallback = makeJsonLambda( - [&fallback_called](const JsonValue&) -> Result { - fallback_called++; - JsonValue result = JsonValue::object(); - result["source"] = JsonValue("fallback"); - return makeSuccess(JsonValue(result)); - }, - "Fallback"); - - auto fallbackLambda = withFallback(primary).orElse(fallback).build(); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - fallbackLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_EQ(result["source"].getString(), "primary"); - EXPECT_EQ(fallback_called.load(), 0); -} - -TEST_F(OrchTest, FallbackUsed) { - // Primary fails, fallback used - auto primary = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result( - Error(OrchError::INTERNAL_ERROR, "Primary failed")); - }, - "Primary"); - - auto fallback1 = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result( - Error(OrchError::INTERNAL_ERROR, "Fallback1 failed")); - }, - "Fallback1"); - - auto fallback2 = makeJsonLambda( - [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["source"] = JsonValue("fallback2"); - return makeSuccess(JsonValue(result)); - }, - "Fallback2"); - - auto fallbackLambda = - withFallback(primary).orElse(fallback1).orElse(fallback2).build(); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - fallbackLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_EQ(result["source"].getString(), "fallback2"); -} - -TEST_F(OrchTest, FallbackExhausted) { - // All fallbacks fail - auto primary = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result(Error(OrchError::INTERNAL_ERROR, "Failed")); - }, - "Primary"); - - auto fallback = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result(Error(OrchError::INTERNAL_ERROR, "Failed")); - }, - "Fallback"); - - auto fallbackLambda = withFallback(primary).orElse(fallback).build(); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - fallbackLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::FALLBACK_EXHAUSTED); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/human_approval_test.cc b/third_party/gopher-orch/tests/gopher/orch/human_approval_test.cc deleted file mode 100644 index 04e06625..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/human_approval_test.cc +++ /dev/null @@ -1,434 +0,0 @@ -// Unit tests for HumanApproval and ApprovalHandler - -#include "orch_test_fixture.h" - -using namespace gopher::orch::human; - -// ============================================================================= -// ApprovalResponse Tests -// ============================================================================= - -TEST_F(OrchTest, ApprovalResponseApprove) { - auto response = ApprovalResponse::approve("User approved"); - - EXPECT_TRUE(response.approved); - EXPECT_EQ(response.reason, "User approved"); - EXPECT_TRUE(response.modifications.isNull()); -} - -TEST_F(OrchTest, ApprovalResponseDeny) { - auto response = ApprovalResponse::deny("User rejected"); - - EXPECT_FALSE(response.approved); - EXPECT_EQ(response.reason, "User rejected"); -} - -TEST_F(OrchTest, ApprovalResponseApproveWithModifications) { - core::JsonValue mods = core::JsonValue::object(); - mods["amount"] = 100; - - auto response = - ApprovalResponse::approveWithModifications(mods, "Reduced amount"); - - EXPECT_TRUE(response.approved); - EXPECT_EQ(response.reason, "Reduced amount"); - EXPECT_FALSE(response.modifications.isNull()); - EXPECT_EQ(response.modifications["amount"].getInt(), 100); -} - -// ============================================================================= -// AutoApprovalHandler Tests -// ============================================================================= - -TEST_F(OrchTest, AutoApprovalHandlerApproves) { - auto handler = std::make_shared("Test auto-approve"); - - ApprovalRequest request; - request.action_name = "dangerous_action"; - request.prompt = "Are you sure?"; - - bool callback_called = false; - ApprovalResponse received_response; - - handler->requestApproval(request, [&](ApprovalResponse response) { - callback_called = true; - received_response = std::move(response); - }); - - EXPECT_TRUE(callback_called); - EXPECT_TRUE(received_response.approved); - EXPECT_EQ(received_response.reason, "Test auto-approve"); -} - -// ============================================================================= -// AutoDenyHandler Tests -// ============================================================================= - -TEST_F(OrchTest, AutoDenyHandlerDenies) { - auto handler = std::make_shared("Security policy"); - - ApprovalRequest request; - request.action_name = "blocked_action"; - - bool callback_called = false; - ApprovalResponse received_response; - - handler->requestApproval(request, [&](ApprovalResponse response) { - callback_called = true; - received_response = std::move(response); - }); - - EXPECT_TRUE(callback_called); - EXPECT_FALSE(received_response.approved); - EXPECT_EQ(received_response.reason, "Security policy"); -} - -// ============================================================================= -// CallbackApprovalHandler Tests -// ============================================================================= - -TEST_F(OrchTest, CallbackApprovalHandlerCustomLogic) { - // Approve only if amount is less than 1000 - auto handler = std::make_shared( - [](const ApprovalRequest& req) -> ApprovalResponse { - if (req.preview.contains("amount")) { - int amount = req.preview["amount"].getInt(); - if (amount < 1000) { - return ApprovalResponse::approve("Amount within limit"); - } else { - return ApprovalResponse::deny("Amount exceeds limit"); - } - } - return ApprovalResponse::approve("No amount specified"); - }); - - // Test with low amount - should approve - ApprovalRequest request1; - request1.preview = core::JsonValue::object(); - request1.preview["amount"] = 500; - - ApprovalResponse response1; - handler->requestApproval( - request1, [&response1](ApprovalResponse r) { response1 = std::move(r); }); - - EXPECT_TRUE(response1.approved); - - // Test with high amount - should deny - ApprovalRequest request2; - request2.preview = core::JsonValue::object(); - request2.preview["amount"] = 2000; - - ApprovalResponse response2; - handler->requestApproval( - request2, [&response2](ApprovalResponse r) { response2 = std::move(r); }); - - EXPECT_FALSE(response2.approved); -} - -// ============================================================================= -// ConditionalApprovalHandler Tests -// ============================================================================= - -TEST_F(OrchTest, ConditionalApprovalHandlerBasic) { - // Approve if action starts with "safe_" - auto handler = std::make_shared( - [](const ApprovalRequest& req) { - return req.action_name.find("safe_") == 0; - }, - "Safe operation", "Unsafe operation blocked"); - - // Test safe action - ApprovalRequest safe_request; - safe_request.action_name = "safe_operation"; - - ApprovalResponse safe_response; - handler->requestApproval(safe_request, [&safe_response](ApprovalResponse r) { - safe_response = std::move(r); - }); - - EXPECT_TRUE(safe_response.approved); - EXPECT_EQ(safe_response.reason, "Safe operation"); - - // Test unsafe action - ApprovalRequest unsafe_request; - unsafe_request.action_name = "dangerous_operation"; - - ApprovalResponse unsafe_response; - handler->requestApproval(unsafe_request, - [&unsafe_response](ApprovalResponse r) { - unsafe_response = std::move(r); - }); - - EXPECT_FALSE(unsafe_response.approved); - EXPECT_EQ(unsafe_response.reason, "Unsafe operation blocked"); -} - -// ============================================================================= -// AsyncCallbackApprovalHandler Tests -// ============================================================================= - -TEST_F(OrchTest, AsyncCallbackApprovalHandlerBasic) { - auto handler = std::make_shared( - [](const ApprovalRequest& req, - std::function callback) { - // Simulate async approval (in real code, this might post to a queue) - callback( - ApprovalResponse::approve("Async approved: " + req.action_name)); - }); - - ApprovalRequest request; - request.action_name = "async_action"; - - ApprovalResponse response; - handler->requestApproval( - request, [&response](ApprovalResponse r) { response = std::move(r); }); - - EXPECT_TRUE(response.approved); - EXPECT_EQ(response.reason, "Async approved: async_action"); -} - -// ============================================================================= -// RecordingApprovalHandler Tests -// ============================================================================= - -TEST_F(OrchTest, RecordingApprovalHandlerRecords) { - auto inner = std::make_shared(); - auto handler = std::make_shared(inner); - - // Make several requests - ApprovalRequest request1; - request1.action_name = "action1"; - handler->requestApproval(request1, [](ApprovalResponse) {}); - - ApprovalRequest request2; - request2.action_name = "action2"; - handler->requestApproval(request2, [](ApprovalResponse) {}); - - ApprovalRequest request3; - request3.action_name = "action3"; - handler->requestApproval(request3, [](ApprovalResponse) {}); - - // Verify recordings - EXPECT_EQ(handler->requestCount(), 3u); - - auto recorded = handler->recordedRequests(); - EXPECT_EQ(recorded[0].action_name, "action1"); - EXPECT_EQ(recorded[1].action_name, "action2"); - EXPECT_EQ(recorded[2].action_name, "action3"); - - // Clear and verify - handler->clearRecords(); - EXPECT_EQ(handler->requestCount(), 0u); -} - -// ============================================================================= -// HumanApproval Runnable Tests -// ============================================================================= - -// Simple test runnable that doubles a number -class DoublerRunnable - : public core::Runnable { - public: - std::string name() const override { return "Doubler"; } - - void invoke(const core::JsonValue& input, - const core::RunnableConfig& config, - core::Dispatcher& dispatcher, - core::ResultCallback callback) override { - (void)config; - dispatcher.post([input, callback]() { - core::JsonValue output = core::JsonValue::object(); - if (input.contains("value")) { - output["result"] = input["value"].getInt() * 2; - } else { - output["result"] = 0; - } - callback(core::makeSuccess(std::move(output))); - }); - } -}; - -TEST_F(OrchTest, HumanApprovalApproved) { - auto inner = std::make_shared(); - auto handler = std::make_shared("User approved"); - - auto approval = HumanApproval::create( - inner, handler, "Double this value?"); - - EXPECT_EQ(approval->name(), "HumanApproval(Doubler)"); - - core::JsonValue input = core::JsonValue::object(); - input["value"] = 21; - - auto result = runToCompletion( - [&](core::Dispatcher& dispatcher, - core::ResultCallback callback) { - approval->invoke(input, core::RunnableConfig(), dispatcher, - std::move(callback)); - }); - - EXPECT_EQ(result["result"].getInt(), 42); -} - -TEST_F(OrchTest, HumanApprovalDenied) { - auto inner = std::make_shared(); - auto handler = std::make_shared("Not authorized"); - - auto approval = HumanApproval::create( - inner, handler, "Double this value?"); - - core::JsonValue input = core::JsonValue::object(); - input["value"] = 21; - - auto result = runToCompletionResult( - [&](core::Dispatcher& dispatcher, - core::ResultCallback callback) { - approval->invoke(input, core::RunnableConfig(), dispatcher, - std::move(callback)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - auto error = mcp::get(result); - EXPECT_EQ(error.code, OrchError::APPROVAL_DENIED); - EXPECT_EQ(error.message, "Not authorized"); -} - -TEST_F(OrchTest, HumanApprovalWithModifications) { - auto inner = std::make_shared(); - - // Handler that modifies the input - auto handler = std::make_shared( - [](const ApprovalRequest& req) -> ApprovalResponse { - (void)req; - // Modify value to 50 instead of original - core::JsonValue mods = core::JsonValue::object(); - mods["value"] = 50; - return ApprovalResponse::approveWithModifications(mods, - "Value adjusted"); - }); - - auto approval = HumanApproval::create( - inner, handler, "Double this value?"); - - core::JsonValue input = core::JsonValue::object(); - input["value"] = 21; // Original value - - auto result = runToCompletion( - [&](core::Dispatcher& dispatcher, - core::ResultCallback callback) { - approval->invoke(input, core::RunnableConfig(), dispatcher, - std::move(callback)); - }); - - // Should be 50 * 2 = 100, not 21 * 2 = 42 - EXPECT_EQ(result["result"].getInt(), 100); -} - -TEST_F(OrchTest, HumanApprovalRequestContainsPreview) { - auto inner = std::make_shared(); - auto recording_handler = std::make_shared( - std::make_shared()); - - auto approval = HumanApproval::create( - inner, recording_handler, "Please approve this operation"); - - core::JsonValue input = core::JsonValue::object(); - input["value"] = 42; - input["description"] = "Test operation"; - - runToCompletion( - [&](core::Dispatcher& dispatcher, - core::ResultCallback callback) { - approval->invoke(input, core::RunnableConfig(), dispatcher, - std::move(callback)); - }); - - // Verify the request was properly formed - EXPECT_EQ(recording_handler->requestCount(), 1u); - auto recorded = recording_handler->recordedRequests(); - EXPECT_EQ(recorded[0].action_name, "Doubler"); - EXPECT_EQ(recorded[0].prompt, "Please approve this operation"); - EXPECT_EQ(recorded[0].preview["value"].getInt(), 42); - EXPECT_EQ(recorded[0].preview["description"].getString(), "Test operation"); -} - -// ============================================================================= -// JsonHumanApproval Alias Test -// ============================================================================= - -TEST_F(OrchTest, JsonHumanApprovalAlias) { - auto inner = std::make_shared(); - auto handler = std::make_shared(); - - // JsonHumanApproval is alias for HumanApproval - auto approval = JsonHumanApproval::create(inner, handler, "Approve?"); - - core::JsonValue input = core::JsonValue::object(); - input["value"] = 10; - - auto result = runToCompletion( - [&](core::Dispatcher& dispatcher, - core::ResultCallback callback) { - approval->invoke(input, core::RunnableConfig(), dispatcher, - std::move(callback)); - }); - - EXPECT_EQ(result["result"].getInt(), 20); -} - -// ============================================================================= -// Integration: HumanApproval with Callback Manager -// ============================================================================= - -TEST_F(OrchTest, HumanApprovalWithCallbackManager) { - auto inner = std::make_shared(); - auto handler = std::make_shared(); - - auto approval = HumanApproval::create( - inner, handler, "Approve?"); - - // Create callback manager to track execution - auto manager = std::make_shared(); - - // Use a recording handler to verify events - class RecordingCallback : public callback::CallbackHandler { - public: - std::vector events; - - void onChainStart(const callback::RunInfo& info, - const core::JsonValue&) override { - events.push_back("start:" + info.name); - } - - void onChainEnd(const callback::RunInfo& info, - const core::JsonValue&) override { - events.push_back("end:" + info.name); - } - }; - - auto recorder = std::make_shared(); - manager->addHandler(recorder); - - core::RunnableConfig config; - config.withCallbacks(manager); - - // Start a chain that wraps the approval - auto run_info = - manager->startChain("approval_test", core::JsonValue::object()); - - core::JsonValue input = core::JsonValue::object(); - input["value"] = 5; - - auto result = runToCompletion( - [&](core::Dispatcher& dispatcher, - core::ResultCallback callback) { - approval->invoke(input, config, dispatcher, std::move(callback)); - }); - - manager->endChain(run_info, result); - - EXPECT_EQ(result["result"].getInt(), 10); - EXPECT_EQ(recorder->events.size(), 2u); - EXPECT_EQ(recorder->events[0], "start:approval_test"); - EXPECT_EQ(recorder->events[1], "end:approval_test"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/integration_test.cc b/third_party/gopher-orch/tests/gopher/orch/integration_test.cc deleted file mode 100644 index e33042e0..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/integration_test.cc +++ /dev/null @@ -1,81 +0,0 @@ -// Integration tests for gopher-orch framework -// Tests combining multiple components together - -#include "orch_test_fixture.h" - -// ============================================================================= -// Integration Tests -// ============================================================================= - -TEST_F(OrchTest, SequenceWithServer) { - // Create a workflow that uses server tools - auto server = makeMockServer("workflow-server"); - - server->addTool("fetch", "Fetch data") - .setHandler("fetch", [](const JsonValue& args) -> Result { - JsonValue result = JsonValue::object(); - result["data"] = JsonValue("fetched-" + args["id"].getString()); - return makeSuccess(JsonValue(result)); - }); - - server->addTool("process", "Process data") - .setHandler("process", [](const JsonValue& args) -> Result { - JsonValue result = JsonValue::object(); - result["processed"] = - JsonValue(args["data"].getString() + "-processed"); - return makeSuccess(JsonValue(result)); - }); - - server->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - // Build workflow: fetch -> process - auto workflow = sequence("FetchAndProcess") - .add(server->tool("fetch")) - .add(server->tool("process")) - .build(); - - JsonValue input = JsonValue::object(); - input["id"] = JsonValue("123"); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - workflow->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["processed"].getString(), "fetched-123-processed"); -} - -TEST_F(OrchTest, ParallelWithServerTools) { - auto server = makeMockServer("parallel-server"); - - server->addTool("tool_a").setHandler( - "tool_a", [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["from"] = JsonValue("tool_a"); - return makeSuccess(JsonValue(result)); - }); - - server->addTool("tool_b").setHandler( - "tool_b", [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["from"] = JsonValue("tool_b"); - return makeSuccess(JsonValue(result)); - }); - - server->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - auto workflow = parallel("ParallelTools") - .add("a", server->tool("tool_a")) - .add("b", server->tool("tool_b")) - .build(); - - JsonValue result = runToCompletion([&](Dispatcher& d, - JsonCallback cb) { - workflow->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["a"]["from"].getString(), "tool_a"); - EXPECT_EQ(result["b"]["from"].getString(), "tool_b"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/lambda_test.cc b/third_party/gopher-orch/tests/gopher/orch/lambda_test.cc deleted file mode 100644 index dd4c2e21..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/lambda_test.cc +++ /dev/null @@ -1,73 +0,0 @@ -// Unit tests for Lambda runnable - -#include "orch_test_fixture.h" - -// ============================================================================= -// Lambda Tests -// ============================================================================= - -TEST_F(OrchTest, LambdaSyncBasic) { - // Create a simple lambda that doubles a number - auto doubler = makeJsonLambda( - [](const JsonValue& input) -> Result { - int value = input["value"].getInt(); - JsonValue result = JsonValue::object(); - result["result"] = JsonValue(value * 2); - return makeSuccess(JsonValue(result)); - }, - "Doubler"); - - EXPECT_EQ(doubler->name(), "Doubler"); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - JsonValue input = JsonValue::object(); - input["value"] = JsonValue(21); - doubler->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["result"].getInt(), 42); -} - -TEST_F(OrchTest, LambdaWithConfig) { - // Lambda that uses config - auto configReader = makeJsonLambda( - [](const JsonValue& input, - const RunnableConfig& config) -> Result { - JsonValue result = JsonValue::object(); - auto tag = config.tag("mode"); - result["mode"] = - JsonValue(tag.has_value() ? tag.value() : std::string("default")); - return makeSuccess(JsonValue(result)); - }, - "ConfigReader"); - - RunnableConfig config; - config.withTag("mode", "test"); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - configReader->invoke(JsonValue::object(), config, d, std::move(cb)); - }); - - EXPECT_EQ(result["mode"].getString(), "test"); -} - -TEST_F(OrchTest, LambdaError) { - auto errorLambda = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result( - Error(OrchError::INVALID_ARGUMENT, "Test error")); - }, - "ErrorLambda"); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - errorLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::INVALID_ARGUMENT); - EXPECT_EQ(mcp::get(result).message, "Test error"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/llm_provider_test.cc b/third_party/gopher-orch/tests/gopher/orch/llm_provider_test.cc deleted file mode 100644 index 57b515fb..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/llm_provider_test.cc +++ /dev/null @@ -1,284 +0,0 @@ -// Unit tests for LLM Providers (OpenAI, Anthropic) - -#include "gopher/orch/llm/anthropic_provider.h" -#include "gopher/orch/llm/openai_provider.h" -#include "mock_http_client.h" -#include "mock_llm_provider.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::llm; - -// ============================================================================= -// MockLLMProvider Tests -// ============================================================================= - -class MockLLMProviderTest : public OrchTest { - protected: - std::shared_ptr provider_; - - void SetUp() override { - OrchTest::SetUp(); - provider_ = makeMockLLMProvider("test-provider"); - } -}; - -TEST_F(MockLLMProviderTest, BasicConfiguration) { - EXPECT_EQ(provider_->name(), "test-provider"); - EXPECT_EQ(provider_->endpoint(), "mock://localhost/v1/chat"); - EXPECT_TRUE(provider_->isConfigured()); - EXPECT_TRUE(provider_->isModelSupported("any-model")); -} - -TEST_F(MockLLMProviderTest, DefaultResponse) { - provider_->setDefaultResponse("Hello from mock!"); - - std::vector messages = {Message::user("Hi")}; - LLMConfig config("test-model"); - - auto response = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - provider_->chat(messages, {}, config, d, std::move(cb)); - }); - - EXPECT_EQ(response.message.content, "Hello from mock!"); - EXPECT_EQ(response.finish_reason, "stop"); - EXPECT_EQ(provider_->callCount(), 1u); -} - -TEST_F(MockLLMProviderTest, QueuedResponses) { - provider_->queueResponse("First response"); - provider_->queueResponse("Second response"); - - std::vector messages = {Message::user("Hi")}; - LLMConfig config("test-model"); - - auto response1 = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - provider_->chat(messages, {}, config, d, std::move(cb)); - }); - EXPECT_EQ(response1.message.content, "First response"); - - auto response2 = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - provider_->chat(messages, {}, config, d, std::move(cb)); - }); - EXPECT_EQ(response2.message.content, "Second response"); - - EXPECT_EQ(provider_->callCount(), 2u); -} - -TEST_F(MockLLMProviderTest, ToolCallResponse) { - std::vector tool_calls; - tool_calls.push_back(ToolCall("call_123", "search", JsonValue::object())); - - provider_->queueToolCalls(tool_calls); - - std::vector messages = {Message::user("Search for something")}; - LLMConfig config("test-model"); - - auto response = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - provider_->chat(messages, {}, config, d, std::move(cb)); - }); - - EXPECT_TRUE(response.hasToolCalls()); - EXPECT_EQ(response.toolCalls().size(), 1u); - EXPECT_EQ(response.toolCalls()[0].name, "search"); - EXPECT_EQ(response.toolCalls()[0].id, "call_123"); - EXPECT_EQ(response.finish_reason, "tool_calls"); -} - -TEST_F(MockLLMProviderTest, ErrorResponse) { - provider_->queueError(LLMError::RATE_LIMITED, "Rate limit exceeded"); - - std::vector messages = {Message::user("Hi")}; - LLMConfig config("test-model"); - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - provider_->chat(messages, {}, config, d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, LLMError::RATE_LIMITED); - EXPECT_EQ(mcp::get(result).message, "Rate limit exceeded"); -} - -TEST_F(MockLLMProviderTest, RecordsLastCall) { - ToolSpec tool1("search", "Search the web", JsonValue::object()); - std::vector tools = {tool1}; - - std::vector messages = {Message::system("You are helpful"), - Message::user("Hello")}; - LLMConfig config("gpt-4"); - config.withTemperature(0.7); - - provider_->setDefaultResponse("OK"); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - provider_->chat(messages, tools, config, d, std::move(cb)); - }); - - EXPECT_EQ(provider_->lastMessages().size(), 2u); - EXPECT_EQ(provider_->lastMessages()[0].role, Role::SYSTEM); - EXPECT_EQ(provider_->lastMessages()[1].content, "Hello"); - - EXPECT_EQ(provider_->lastTools().size(), 1u); - EXPECT_EQ(provider_->lastTools()[0].name, "search"); - - EXPECT_EQ(provider_->lastConfig().model, "gpt-4"); - EXPECT_TRUE(provider_->lastConfig().temperature.has_value()); - EXPECT_DOUBLE_EQ(*provider_->lastConfig().temperature, 0.7); -} - -TEST_F(MockLLMProviderTest, Reset) { - provider_->queueResponse("Test"); - - std::vector messages = {Message::user("Hi")}; - LLMConfig config("test-model"); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - provider_->chat(messages, {}, config, d, std::move(cb)); - }); - - EXPECT_EQ(provider_->callCount(), 1u); - EXPECT_FALSE(provider_->lastMessages().empty()); - - provider_->reset(); - - EXPECT_EQ(provider_->callCount(), 0u); - EXPECT_TRUE(provider_->lastMessages().empty()); -} - -// ============================================================================= -// LLM Type Tests -// ============================================================================= - -TEST(LLMTypesTest, MessageFactoryMethods) { - auto system = Message::system("System prompt"); - EXPECT_EQ(system.role, Role::SYSTEM); - EXPECT_EQ(system.content, "System prompt"); - - auto user = Message::user("User input"); - EXPECT_EQ(user.role, Role::USER); - EXPECT_EQ(user.content, "User input"); - - auto assistant = Message::assistant("Response"); - EXPECT_EQ(assistant.role, Role::ASSISTANT); - EXPECT_EQ(assistant.content, "Response"); - - auto tool_result = Message::toolResult("call_123", "Tool output"); - EXPECT_EQ(tool_result.role, Role::TOOL); - EXPECT_EQ(tool_result.content, "Tool output"); - EXPECT_TRUE(tool_result.tool_call_id.has_value()); - EXPECT_EQ(*tool_result.tool_call_id, "call_123"); -} - -TEST(LLMTypesTest, MessageWithToolCalls) { - std::vector calls; - calls.push_back(ToolCall("id1", "tool1", JsonValue::object())); - calls.push_back(ToolCall("id2", "tool2", JsonValue::object())); - - auto msg = Message::assistantWithToolCalls(calls); - EXPECT_EQ(msg.role, Role::ASSISTANT); - EXPECT_TRUE(msg.hasToolCalls()); - EXPECT_EQ(msg.tool_calls->size(), 2u); - EXPECT_EQ((*msg.tool_calls)[0].name, "tool1"); - EXPECT_EQ((*msg.tool_calls)[1].name, "tool2"); -} - -TEST(LLMTypesTest, RoleConversion) { - EXPECT_EQ(roleToString(Role::SYSTEM), "system"); - EXPECT_EQ(roleToString(Role::USER), "user"); - EXPECT_EQ(roleToString(Role::ASSISTANT), "assistant"); - EXPECT_EQ(roleToString(Role::TOOL), "tool"); - - EXPECT_EQ(parseRole("system"), Role::SYSTEM); - EXPECT_EQ(parseRole("user"), Role::USER); - EXPECT_EQ(parseRole("assistant"), Role::ASSISTANT); - EXPECT_EQ(parseRole("tool"), Role::TOOL); - EXPECT_EQ(parseRole("unknown"), Role::USER); // Default -} - -TEST(LLMTypesTest, LLMConfigBuilder) { - LLMConfig config("gpt-4"); - config.withTemperature(0.8) - .withMaxTokens(2000) - .withTopP(0.95) - .withSeed(42) - .withStop({"END", "STOP"}) - .withTimeout(std::chrono::milliseconds(30000)); - - EXPECT_EQ(config.model, "gpt-4"); - EXPECT_TRUE(config.temperature.has_value()); - EXPECT_DOUBLE_EQ(*config.temperature, 0.8); - EXPECT_TRUE(config.max_tokens.has_value()); - EXPECT_EQ(*config.max_tokens, 2000); - EXPECT_TRUE(config.top_p.has_value()); - EXPECT_DOUBLE_EQ(*config.top_p, 0.95); - EXPECT_TRUE(config.seed.has_value()); - EXPECT_EQ(*config.seed, 42); - EXPECT_TRUE(config.stop.has_value()); - EXPECT_EQ(config.stop->size(), 2u); - EXPECT_EQ(config.timeout, std::chrono::milliseconds(30000)); -} - -TEST(LLMTypesTest, LLMResponse) { - LLMResponse response; - response.message = Message::assistant("Hello"); - response.finish_reason = "stop"; - response.usage = Usage(100, 50); - - EXPECT_EQ(response.message.content, "Hello"); - EXPECT_FALSE(response.hasToolCalls()); - EXPECT_TRUE(response.isComplete()); - EXPECT_FALSE(response.isTruncated()); - - EXPECT_TRUE(response.usage.has_value()); - EXPECT_EQ(response.usage->prompt_tokens, 100); - EXPECT_EQ(response.usage->completion_tokens, 50); - EXPECT_EQ(response.usage->total_tokens, 150); -} - -TEST(LLMTypesTest, LLMResponseTruncated) { - LLMResponse response; - response.finish_reason = "length"; - - EXPECT_FALSE(response.isComplete()); - EXPECT_TRUE(response.isTruncated()); -} - -TEST(LLMTypesTest, ToolSpec) { - JsonValue params = JsonValue::object(); - params["type"] = "object"; - JsonValue props = JsonValue::object(); - JsonValue query_prop = JsonValue::object(); - query_prop["type"] = "string"; - props["query"] = query_prop; - params["properties"] = props; - - ToolSpec spec("search", "Search the web", params); - - EXPECT_EQ(spec.name, "search"); - EXPECT_EQ(spec.description, "Search the web"); - EXPECT_TRUE(spec.parameters.contains("type")); - EXPECT_EQ(spec.parameters["type"].getString(), "object"); -} - -// ============================================================================= -// ProviderConfig Tests -// ============================================================================= - -TEST(ProviderConfigTest, Builder) { - ProviderConfig config(ProviderType::OPENAI); - config.withApiKey("sk-test") - .withBaseUrl("https://custom.api.com") - .withHeader("X-Custom", "value"); - - EXPECT_EQ(config.type, ProviderType::OPENAI); - EXPECT_EQ(config.api_key, "sk-test"); - EXPECT_EQ(config.base_url, "https://custom.api.com"); - EXPECT_EQ(config.headers["X-Custom"], "value"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/llm_runnable_test.cc b/third_party/gopher-orch/tests/gopher/orch/llm_runnable_test.cc deleted file mode 100644 index d1cae780..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/llm_runnable_test.cc +++ /dev/null @@ -1,332 +0,0 @@ -// Unit tests for LLMRunnable - -#include "gopher/orch/llm/llm_runnable.h" - -#include "mock_llm_provider.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -// ============================================================================= -// LLMRunnable Tests -// ============================================================================= - -class LLMRunnableTest : public OrchTest { - protected: - std::shared_ptr mock_provider_; - LLMRunnable::Ptr llm_runnable_; - - void SetUp() override { - OrchTest::SetUp(); - mock_provider_ = makeMockLLMProvider("test-provider"); - llm_runnable_ = LLMRunnable::create(mock_provider_, LLMConfig("gpt-4")); - } -}; - -TEST_F(LLMRunnableTest, Name) { - EXPECT_EQ(llm_runnable_->name(), "LLMRunnable(test-provider)"); -} - -TEST_F(LLMRunnableTest, SimpleStringInput) { - mock_provider_->setDefaultResponse("Hello back!"); - - JsonValue input = "Hello, how are you?"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // Verify output structure - EXPECT_TRUE(result.isObject()); - EXPECT_TRUE(result.contains("message")); - EXPECT_TRUE(result.contains("finish_reason")); - - EXPECT_EQ(result["message"]["content"].getString(), "Hello back!"); - EXPECT_EQ(result["message"]["role"].getString(), "assistant"); - EXPECT_EQ(result["finish_reason"].getString(), "stop"); - - // Verify the provider received correct input - EXPECT_EQ(mock_provider_->lastMessages().size(), 1u); - EXPECT_EQ(mock_provider_->lastMessages()[0].role, Role::USER); - EXPECT_EQ(mock_provider_->lastMessages()[0].content, "Hello, how are you?"); -} - -TEST_F(LLMRunnableTest, MessagesArrayInput) { - mock_provider_->setDefaultResponse("I can help with that."); - - JsonValue input = JsonValue::object(); - JsonValue messages = JsonValue::array(); - - JsonValue system_msg = JsonValue::object(); - system_msg["role"] = "system"; - system_msg["content"] = "You are a helpful assistant."; - messages.push_back(system_msg); - - JsonValue user_msg = JsonValue::object(); - user_msg["role"] = "user"; - user_msg["content"] = "Help me with coding."; - messages.push_back(user_msg); - - input["messages"] = messages; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["message"]["content"].getString(), "I can help with that."); - - // Verify messages were passed correctly - auto last_msgs = mock_provider_->lastMessages(); - EXPECT_EQ(last_msgs.size(), 2u); - EXPECT_EQ(last_msgs[0].role, Role::SYSTEM); - EXPECT_EQ(last_msgs[0].content, "You are a helpful assistant."); - EXPECT_EQ(last_msgs[1].role, Role::USER); - EXPECT_EQ(last_msgs[1].content, "Help me with coding."); -} - -TEST_F(LLMRunnableTest, WithTools) { - mock_provider_->setDefaultResponse("I'll search for that."); - - JsonValue input = JsonValue::object(); - JsonValue messages = JsonValue::array(); - JsonValue user_msg = JsonValue::object(); - user_msg["role"] = "user"; - user_msg["content"] = "Search for weather"; - messages.push_back(user_msg); - input["messages"] = messages; - - // Add tools - JsonValue tools = JsonValue::array(); - JsonValue tool = JsonValue::object(); - tool["name"] = "search"; - tool["description"] = "Search the web"; - JsonValue params = JsonValue::object(); - params["type"] = "object"; - tool["parameters"] = params; - tools.push_back(tool); - input["tools"] = tools; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // Verify tools were passed to provider - auto last_tools = mock_provider_->lastTools(); - EXPECT_EQ(last_tools.size(), 1u); - EXPECT_EQ(last_tools[0].name, "search"); - EXPECT_EQ(last_tools[0].description, "Search the web"); -} - -TEST_F(LLMRunnableTest, ToolCallResponse) { - std::vector tool_calls; - JsonValue args = JsonValue::object(); - args["query"] = "weather in tokyo"; - tool_calls.push_back(ToolCall("call_123", "search", args)); - mock_provider_->queueToolCalls(tool_calls); - - JsonValue input = "What's the weather in Tokyo?"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["finish_reason"].getString(), "tool_calls"); - EXPECT_TRUE(result["message"].contains("tool_calls")); - EXPECT_TRUE(result["message"]["tool_calls"].isArray()); - EXPECT_EQ(result["message"]["tool_calls"].size(), 1u); - - auto tool_call = result["message"]["tool_calls"][0]; - EXPECT_EQ(tool_call["id"].getString(), "call_123"); - EXPECT_EQ(tool_call["name"].getString(), "search"); - EXPECT_EQ(tool_call["arguments"]["query"].getString(), "weather in tokyo"); -} - -TEST_F(LLMRunnableTest, ConfigOverrides) { - mock_provider_->setDefaultResponse("OK"); - - JsonValue input = JsonValue::object(); - JsonValue messages = JsonValue::array(); - JsonValue user_msg = JsonValue::object(); - user_msg["role"] = "user"; - user_msg["content"] = "Hi"; - messages.push_back(user_msg); - input["messages"] = messages; - - // Override config - JsonValue config = JsonValue::object(); - config["model"] = "gpt-3.5-turbo"; - config["temperature"] = 0.5; - config["max_tokens"] = 100; - input["config"] = config; - - runToCompletion([&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - auto last_config = mock_provider_->lastConfig(); - EXPECT_EQ(last_config.model, "gpt-3.5-turbo"); - EXPECT_TRUE(last_config.temperature.has_value()); - EXPECT_DOUBLE_EQ(*last_config.temperature, 0.5); - EXPECT_TRUE(last_config.max_tokens.has_value()); - EXPECT_EQ(*last_config.max_tokens, 100); -} - -TEST_F(LLMRunnableTest, DefaultConfigUsed) { - LLMConfig default_config("claude-3"); - default_config.withTemperature(0.8); - llm_runnable_->setDefaultConfig(default_config); - - mock_provider_->setDefaultResponse("OK"); - - JsonValue input = "Hello"; - - runToCompletion([&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - auto last_config = mock_provider_->lastConfig(); - EXPECT_EQ(last_config.model, "claude-3"); - EXPECT_TRUE(last_config.temperature.has_value()); - EXPECT_DOUBLE_EQ(*last_config.temperature, 0.8); -} - -TEST_F(LLMRunnableTest, UsageIncluded) { - LLMResponse response; - response.message = Message::assistant("Test response"); - response.finish_reason = "stop"; - response.usage = Usage(100, 50); - mock_provider_->queueFullResponse(response); - - JsonValue input = "Test"; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result.contains("usage")); - EXPECT_EQ(result["usage"]["prompt_tokens"].getInt(), 100); - EXPECT_EQ(result["usage"]["completion_tokens"].getInt(), 50); - EXPECT_EQ(result["usage"]["total_tokens"].getInt(), 150); -} - -TEST_F(LLMRunnableTest, ErrorPropagation) { - mock_provider_->queueError(LLMError::RATE_LIMITED, "Rate limit exceeded"); - - JsonValue input = "Test"; - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, LLMError::RATE_LIMITED); - EXPECT_EQ(mcp::get(result).message, "Rate limit exceeded"); -} - -TEST_F(LLMRunnableTest, NoProviderError) { - auto llm_no_provider = LLMRunnable::create(nullptr, LLMConfig("gpt-4")); - - JsonValue input = "Test"; - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - llm_no_provider->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "No LLM provider configured"); -} - -TEST_F(LLMRunnableTest, EmptyMessagesError) { - mock_provider_->setDefaultResponse("OK"); - - // Empty object input with no messages - JsonValue input = JsonValue::object(); - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "No messages provided"); -} - -TEST_F(LLMRunnableTest, ToolResultMessageParsing) { - mock_provider_->setDefaultResponse("Based on the search results..."); - - JsonValue input = JsonValue::object(); - JsonValue messages = JsonValue::array(); - - // User message - JsonValue user_msg = JsonValue::object(); - user_msg["role"] = "user"; - user_msg["content"] = "Search for weather"; - messages.push_back(user_msg); - - // Assistant message with tool calls - JsonValue assistant_msg = JsonValue::object(); - assistant_msg["role"] = "assistant"; - assistant_msg["content"] = ""; - JsonValue tool_calls = JsonValue::array(); - JsonValue call = JsonValue::object(); - call["id"] = "call_123"; - call["name"] = "search"; - JsonValue args = JsonValue::object(); - args["query"] = "weather"; - call["arguments"] = args; - tool_calls.push_back(call); - assistant_msg["tool_calls"] = tool_calls; - messages.push_back(assistant_msg); - - // Tool result message - JsonValue tool_msg = JsonValue::object(); - tool_msg["role"] = "tool"; - tool_msg["content"] = "Sunny, 25C"; - tool_msg["tool_call_id"] = "call_123"; - messages.push_back(tool_msg); - - input["messages"] = messages; - - runToCompletion([&](Dispatcher& d, ResultCallback cb) { - llm_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - auto last_msgs = mock_provider_->lastMessages(); - EXPECT_EQ(last_msgs.size(), 3u); - - // Verify tool result message - EXPECT_EQ(last_msgs[2].role, Role::TOOL); - EXPECT_EQ(last_msgs[2].content, "Sunny, 25C"); - EXPECT_TRUE(last_msgs[2].tool_call_id.has_value()); - EXPECT_EQ(*last_msgs[2].tool_call_id, "call_123"); - - // Verify assistant message with tool calls - EXPECT_EQ(last_msgs[1].role, Role::ASSISTANT); - EXPECT_TRUE(last_msgs[1].hasToolCalls()); - EXPECT_EQ(last_msgs[1].tool_calls->size(), 1u); - EXPECT_EQ((*last_msgs[1].tool_calls)[0].name, "search"); -} - -TEST_F(LLMRunnableTest, Accessors) { - EXPECT_EQ(llm_runnable_->provider(), mock_provider_); - EXPECT_EQ(llm_runnable_->defaultConfig().model, "gpt-4"); -} - -// ============================================================================= -// Factory Function Test -// ============================================================================= - -TEST_F(LLMRunnableTest, MakeLLMRunnable) { - auto llm = makeLLMRunnable(mock_provider_, LLMConfig("test-model")); - EXPECT_NE(llm, nullptr); - EXPECT_EQ(llm->provider(), mock_provider_); - EXPECT_EQ(llm->defaultConfig().model, "test-model"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/mcp_server_test.cc b/third_party/gopher-orch/tests/gopher/orch/mcp_server_test.cc deleted file mode 100644 index 8f8f11ba..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/mcp_server_test.cc +++ /dev/null @@ -1,106 +0,0 @@ -// Unit tests for MCPServer -// -// Tests MCPServer configuration, creation, and integration with -// ServerComposite. Note: Full integration tests require actual MCP server -// connections. - -#include "orch_test_fixture.h" - -#ifdef GOPHER_ORCH_WITH_MCP -#include "gopher/orch/server/mcp_server.h" -#endif - -// ============================================================================= -// MCPServer Configuration Tests -// ============================================================================= - -#ifdef GOPHER_ORCH_WITH_MCP - -TEST_F(OrchTest, MCPServerConfigDefaults) { - // Test that MCPServerConfig has sensible defaults - server::MCPServerConfig config; - config.name = "test-server"; - - EXPECT_EQ(config.name, "test-server"); - EXPECT_EQ(config.transport_type, - server::MCPServerConfig::TransportType::STDIO); - EXPECT_EQ(config.client_name, "gopher-orch"); - EXPECT_EQ(config.client_version, "1.0.0"); - EXPECT_EQ(config.max_connect_retries, 3u); - EXPECT_EQ(config.connect_timeout.count(), 30000); - EXPECT_EQ(config.request_timeout.count(), 60000); -} - -TEST_F(OrchTest, MCPServerConfigStdioTransport) { - // Test stdio transport configuration - server::MCPServerConfig config; - config.name = "npx-server"; - config.transport_type = server::MCPServerConfig::TransportType::STDIO; - config.stdio_transport.command = "npx"; - config.stdio_transport.args = {"-y", - "@modelcontextprotocol/server-everything"}; - config.stdio_transport.env["NODE_ENV"] = "production"; - - EXPECT_EQ(config.stdio_transport.command, "npx"); - EXPECT_EQ(config.stdio_transport.args.size(), 2u); - EXPECT_EQ(config.stdio_transport.args[0], "-y"); - EXPECT_EQ(config.stdio_transport.env["NODE_ENV"], "production"); -} - -TEST_F(OrchTest, MCPServerConfigHttpSseTransport) { - // Test HTTP+SSE transport configuration - server::MCPServerConfig config; - config.name = "remote-server"; - config.transport_type = server::MCPServerConfig::TransportType::HTTP_SSE; - config.http_sse_transport.url = "https://api.example.com/mcp"; - config.http_sse_transport.headers["Authorization"] = "Bearer token123"; - config.http_sse_transport.verify_ssl = true; - - EXPECT_EQ(config.http_sse_transport.url, "https://api.example.com/mcp"); - EXPECT_EQ(config.http_sse_transport.headers["Authorization"], - "Bearer token123"); - EXPECT_TRUE(config.http_sse_transport.verify_ssl); -} - -TEST_F(OrchTest, MCPServerWithComposite) { - // Test that Server interface can be used with ServerComposite - // Uses mock server since MCPServer requires actual MCP connection - auto mockServer = makeMockServer("mcp-like-server"); - mockServer->addTool("get_weather", "Get weather for a location"); - mockServer->setHandler("get_weather", - [](const JsonValue& args) -> Result { - JsonValue result = JsonValue::object(); - result["temperature"] = JsonValue(72); - result["location"] = args["city"]; - return makeSuccess(JsonValue(result)); - }); - - // Create composite and add the server - auto composite = ServerComposite::create("multi-server"); - std::vector tools = {"get_weather"}; - composite->addServer(mockServer, tools, true); - - // Connect - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - composite->connectAll(d, std::move(cb)); - }); - - // Get tool through composite - auto weatherTool = composite->tool("mcp-like-server.get_weather"); - ASSERT_NE(weatherTool, nullptr); - - // Invoke the tool - JsonValue input = JsonValue::object(); - input["city"] = JsonValue("Seattle"); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - weatherTool->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["temperature"].getInt(), 72); - EXPECT_EQ(result["location"].getString(), "Seattle"); -} - -#endif // GOPHER_ORCH_WITH_MCP diff --git a/third_party/gopher-orch/tests/gopher/orch/mock_http_client.h b/third_party/gopher-orch/tests/gopher/orch/mock_http_client.h deleted file mode 100644 index 62d1dded..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/mock_http_client.h +++ /dev/null @@ -1,232 +0,0 @@ -// MockHttpClient - Mock HTTP client for testing REST endpoints -// -// Provides configurable HTTP responses for testing without network calls. -// Supports: -// - Pre-configured responses per URL/method -// - Request recording for verification -// - Error simulation -// - Response delays - -#pragma once - -#include -#include -#include -#include - -#include "gopher/orch/server/rest_server.h" - -namespace gopher { -namespace orch { -namespace server { - -// Request record for verification -struct HttpRequestRecord { - HttpMethod method; - std::string url; - std::map headers; - std::string body; -}; - -// Mock response configuration -struct MockHttpResponseConfig { - HttpResponse response; - optional error; - std::chrono::milliseconds delay{0}; -}; - -// MockHttpClient - In-memory HTTP client for testing -class MockHttpClient : public HttpClient { - public: - MockHttpClient() = default; - - void request(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - ResponseCallback callback) override { - std::lock_guard lock(mutex_); - - // Record the request - HttpRequestRecord record; - record.method = method; - record.url = url; - record.headers = headers; - record.body = body; - requests_.push_back(record); - - // Build key for response lookup - std::string key = httpMethodToString(method) + " " + url; - - // Look for exact match first, then prefix match - MockHttpResponseConfig response_config; - auto it = responses_.find(key); - if (it != responses_.end()) { - response_config = it->second; - } else { - // Try prefix match - for (const auto& kv : responses_) { - if (key.find(kv.first) == 0 || kv.first.find(key) == 0) { - response_config = kv.second; - break; - } - } - // If no match and default is set - if (default_response_.has_value()) { - response_config.response = *default_response_; - } else { - // Default 404 response - response_config.response.status_code = 404; - response_config.response.body = "{\"error\": \"Not found\"}"; - } - } - - // Schedule response - if (response_config.delay.count() > 0) { - auto timer = dispatcher.createTimer([callback = std::move(callback), - response_config]() mutable { - if (response_config.error.has_value()) { - callback(Result(*response_config.error)); - } else { - callback(Result(std::move(response_config.response))); - } - }); - timer->enableTimer(response_config.delay); - } else { - dispatcher.post([callback = std::move(callback), - response_config]() mutable { - if (response_config.error.has_value()) { - callback(Result(*response_config.error)); - } else { - callback(Result(std::move(response_config.response))); - } - }); - } - } - - // ========================================================================= - // MockHttpClient-specific API for test configuration - // ========================================================================= - - // Set response for a specific URL/method - MockHttpClient& setResponse(HttpMethod method, - const std::string& url, - int status_code, - const std::string& body) { - std::lock_guard lock(mutex_); - std::string key = httpMethodToString(method) + " " + url; - MockHttpResponseConfig config; - config.response.status_code = status_code; - config.response.body = body; - responses_[key] = config; - return *this; - } - - // Set response with headers - MockHttpClient& setResponse( - HttpMethod method, - const std::string& url, - int status_code, - const std::string& body, - const std::map& headers) { - std::lock_guard lock(mutex_); - std::string key = httpMethodToString(method) + " " + url; - MockHttpResponseConfig config; - config.response.status_code = status_code; - config.response.body = body; - config.response.headers = headers; - responses_[key] = config; - return *this; - } - - // Set error for a specific URL/method - MockHttpClient& setError(HttpMethod method, - const std::string& url, - int code, - const std::string& message) { - std::lock_guard lock(mutex_); - std::string key = httpMethodToString(method) + " " + url; - MockHttpResponseConfig config; - config.error = Error(code, message); - responses_[key] = config; - return *this; - } - - // Set default response for unmatched requests - MockHttpClient& setDefaultResponse(int status_code, const std::string& body) { - std::lock_guard lock(mutex_); - HttpResponse response; - response.status_code = status_code; - response.body = body; - default_response_ = response; - return *this; - } - - // Set response delay - MockHttpClient& setDelay(HttpMethod method, - const std::string& url, - std::chrono::milliseconds delay) { - std::lock_guard lock(mutex_); - std::string key = httpMethodToString(method) + " " + url; - if (responses_.find(key) != responses_.end()) { - responses_[key].delay = delay; - } - return *this; - } - - // Get all recorded requests - std::vector requests() const { - std::lock_guard lock(mutex_); - return requests_; - } - - // Get request count - size_t requestCount() const { - std::lock_guard lock(mutex_); - return requests_.size(); - } - - // Get last request - optional lastRequest() const { - std::lock_guard lock(mutex_); - if (requests_.empty()) { - return nullopt; - } - return requests_.back(); - } - - // Check if a specific URL was called - bool wasCalled(HttpMethod method, const std::string& url) const { - std::lock_guard lock(mutex_); - for (const auto& req : requests_) { - if (req.method == method && req.url == url) { - return true; - } - } - return false; - } - - // Reset mock state - void reset() { - std::lock_guard lock(mutex_); - requests_.clear(); - responses_.clear(); - default_response_ = nullopt; - } - - private: - mutable std::mutex mutex_; - std::vector requests_; - std::map responses_; - optional default_response_; -}; - -// Factory function -inline std::shared_ptr makeMockHttpClient() { - return std::make_shared(); -} - -} // namespace server -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/tests/gopher/orch/mock_llm_provider.h b/third_party/gopher-orch/tests/gopher/orch/mock_llm_provider.h deleted file mode 100644 index ff0fa33b..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/mock_llm_provider.h +++ /dev/null @@ -1,238 +0,0 @@ -// MockLLMProvider - Mock LLM provider for testing agents and tool execution -// -// Provides configurable responses for testing without network calls. -// Supports: -// - Pre-configured responses -// - Tool call simulation -// - Response sequences -// - Error simulation - -#pragma once - -#include -#include -#include -#include - -#include "gopher/orch/llm/llm_provider.h" - -namespace gopher { -namespace orch { -namespace llm { - -// Mock response configuration -struct MockResponseConfig { - LLMResponse response; - optional error; - std::chrono::milliseconds delay{0}; -}; - -// MockLLMProvider - In-memory LLM provider for testing -class MockLLMProvider : public LLMProvider { - public: - using Ptr = std::shared_ptr; - - explicit MockLLMProvider(const std::string& name = "mock-llm") - : name_(name) {} - - // LLMProvider interface - std::string name() const override { return name_; } - - void chat(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - ChatCallback callback) override { - std::lock_guard lock(mutex_); - - call_count_++; - last_messages_ = messages; - last_tools_ = tools; - last_config_ = config; - - // Get next response from queue, or use default - MockResponseConfig response_config; - if (!response_queue_.empty()) { - response_config = response_queue_.front(); - response_queue_.pop(); - } else if (default_response_.has_value()) { - response_config.response = *default_response_; - } else { - // Default: return empty response - response_config.response.message = - Message::assistant("Default mock response"); - response_config.response.finish_reason = "stop"; - } - - // Schedule response with optional delay - if (response_config.delay.count() > 0) { - auto timer = dispatcher.createTimer([callback = std::move(callback), - response_config]() mutable { - if (response_config.error.has_value()) { - callback(Result(*response_config.error)); - } else { - callback(Result(std::move(response_config.response))); - } - }); - timer->enableTimer(response_config.delay); - } else { - dispatcher.post([callback = std::move(callback), - response_config]() mutable { - if (response_config.error.has_value()) { - callback(Result(*response_config.error)); - } else { - callback(Result(std::move(response_config.response))); - } - }); - } - } - - void chatStream(const std::vector& messages, - const std::vector& tools, - const LLMConfig& config, - Dispatcher& dispatcher, - StreamCallback on_chunk, - ChatCallback on_complete) override { - // Fall back to non-streaming - chat(messages, tools, config, dispatcher, std::move(on_complete)); - } - - bool isModelSupported(const std::string& model) const override { - return !model.empty(); - } - - std::vector supportedModels() const override { - return {"mock-model", "test-model"}; - } - - std::string endpoint() const override { return "mock://localhost/v1/chat"; } - - bool isConfigured() const override { return true; } - - // ========================================================================= - // MockLLMProvider-specific API for test configuration - // ========================================================================= - - // Set default response for all calls - MockLLMProvider& setDefaultResponse(const std::string& content) { - std::lock_guard lock(mutex_); - LLMResponse response; - response.message = Message::assistant(content); - response.finish_reason = "stop"; - default_response_ = response; - return *this; - } - - // Set default response with tool calls - MockLLMProvider& setDefaultToolCalls( - const std::vector& tool_calls) { - std::lock_guard lock(mutex_); - LLMResponse response; - response.message = Message::assistantWithToolCalls(tool_calls); - response.finish_reason = "tool_calls"; - default_response_ = response; - return *this; - } - - // Queue a response (FIFO order) - MockLLMProvider& queueResponse(const std::string& content) { - std::lock_guard lock(mutex_); - MockResponseConfig config; - config.response.message = Message::assistant(content); - config.response.finish_reason = "stop"; - response_queue_.push(config); - return *this; - } - - // Queue a tool call response - MockLLMProvider& queueToolCalls(const std::vector& tool_calls) { - std::lock_guard lock(mutex_); - MockResponseConfig config; - config.response.message = Message::assistantWithToolCalls(tool_calls); - config.response.finish_reason = "tool_calls"; - response_queue_.push(config); - return *this; - } - - // Queue an error response - MockLLMProvider& queueError(int code, const std::string& message) { - std::lock_guard lock(mutex_); - MockResponseConfig config; - config.error = Error(code, message); - response_queue_.push(config); - return *this; - } - - // Queue a full LLMResponse - MockLLMProvider& queueFullResponse(const LLMResponse& response) { - std::lock_guard lock(mutex_); - MockResponseConfig config; - config.response = response; - response_queue_.push(config); - return *this; - } - - // Set response delay - MockLLMProvider& setDelay(std::chrono::milliseconds delay) { - std::lock_guard lock(mutex_); - delay_ = delay; - return *this; - } - - // Get call count - size_t callCount() const { - std::lock_guard lock(mutex_); - return call_count_; - } - - // Get last messages received - std::vector lastMessages() const { - std::lock_guard lock(mutex_); - return last_messages_; - } - - // Get last tools received - std::vector lastTools() const { - std::lock_guard lock(mutex_); - return last_tools_; - } - - // Get last config received - LLMConfig lastConfig() const { - std::lock_guard lock(mutex_); - return last_config_; - } - - // Reset mock state - void reset() { - std::lock_guard lock(mutex_); - call_count_ = 0; - last_messages_.clear(); - last_tools_.clear(); - default_response_ = nullopt; - while (!response_queue_.empty()) { - response_queue_.pop(); - } - } - - private: - mutable std::mutex mutex_; - std::string name_; - size_t call_count_ = 0; - std::vector last_messages_; - std::vector last_tools_; - LLMConfig last_config_; - optional default_response_; - std::queue response_queue_; - std::chrono::milliseconds delay_{0}; -}; - -// Factory function -inline std::shared_ptr makeMockLLMProvider( - const std::string& name = "mock-llm") { - return std::make_shared(name); -} - -} // namespace llm -} // namespace orch -} // namespace gopher diff --git a/third_party/gopher-orch/tests/gopher/orch/mock_server_test.cc b/third_party/gopher-orch/tests/gopher/orch/mock_server_test.cc deleted file mode 100644 index ca2d5daf..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/mock_server_test.cc +++ /dev/null @@ -1,105 +0,0 @@ -// Unit tests for MockServer - -#include "orch_test_fixture.h" - -// ============================================================================= -// MockServer Tests -// ============================================================================= - -TEST_F(OrchTest, MockServerBasic) { - auto server = makeMockServer("test-server"); - - JsonValue response = JsonValue::object(); - response["message"] = JsonValue("Hello!"); - - server->addTool("greet", "Greets a person").setResponse("greet", response); - - EXPECT_EQ(server->name(), "test-server"); - EXPECT_EQ(server->connectionState(), ConnectionState::DISCONNECTED); - - // Connect - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - EXPECT_TRUE(server->isConnected()); - - // List tools - auto tools = runToCompletion>( - [&](Dispatcher& d, ServerToolListCallback cb) { - server->listTools(d, std::move(cb)); - }); - - EXPECT_EQ(tools.size(), 1u); - EXPECT_EQ(tools[0].name, "greet"); - - // Get tool - auto greet = server->tool("greet"); - EXPECT_NE(greet, nullptr); - EXPECT_EQ(greet->name(), "greet"); - - // Call tool - JsonValue toolResult = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - greet->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(toolResult["message"].getString(), "Hello!"); - EXPECT_EQ(server->callCount("greet"), 1u); -} - -TEST_F(OrchTest, MockServerCustomHandler) { - auto server = makeMockServer("handler-server"); - - server->addTool("echo").setHandler( - "echo", [](const JsonValue& args) -> Result { - JsonValue result = JsonValue::object(); - result["echoed"] = args; - return makeSuccess(JsonValue(result)); - }); - - server->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - auto echo = server->tool("echo"); - - JsonValue input = JsonValue::object(); - input["data"] = JsonValue("test"); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - echo->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["echoed"]["data"].getString(), "test"); -} - -TEST_F(OrchTest, MockServerToolNotFound) { - auto server = makeMockServer("empty-server"); - server->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - EXPECT_EQ(server->tool("nonexistent"), nullptr); -} - -TEST_F(OrchTest, MockServerError) { - auto server = makeMockServer("error-server"); - - server->addTool("fail").setError("fail", OrchError::INTERNAL_ERROR, - "Simulated failure"); - - server->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - auto fail = server->tool("fail"); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - fail->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::INTERNAL_ERROR); - EXPECT_EQ(mcp::get(result).message, "Simulated failure"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/orch_test_fixture.h b/third_party/gopher-orch/tests/gopher/orch/orch_test_fixture.h deleted file mode 100644 index 752448bd..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/orch_test_fixture.h +++ /dev/null @@ -1,95 +0,0 @@ -#pragma once - -// Shared test fixture for gopher-orch unit tests -// Provides common dispatcher setup and async helpers - -#include -#include -#include -#include -#include - -#include "mcp/event/libevent_dispatcher.h" - -#include "gopher/orch/orch.h" -#include "gtest/gtest.h" - -using namespace gopher::orch; -using namespace gopher::orch::core; -using namespace gopher::orch::composition; -using namespace gopher::orch::resilience; -using namespace gopher::orch::server; - -// Test fixture with dispatcher -class OrchTest : public ::testing::Test { - protected: - void SetUp() override { - dispatcher_ = std::make_unique("test"); - } - - void TearDown() override { dispatcher_.reset(); } - - // Run dispatcher until callback completes - template - T runToCompletion( - std::function)> operation) { - std::mutex mutex; - std::condition_variable cv; - bool done = false; - Result result = Result(Error(-1, "Not completed")); - - operation(*dispatcher_, [&](Result r) { - std::lock_guard lock(mutex); - result = std::move(r); - done = true; - cv.notify_one(); - }); - - // Run dispatcher until done - while (true) { - { - std::unique_lock lock(mutex); - if (done) - break; - } - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - EXPECT_TRUE(mcp::holds_alternative(result)) - << "Operation failed: " << mcp::get(result).message; - return mcp::get(result); - } - - // Run dispatcher until callback completes (allow error) - template - Result runToCompletionResult( - std::function)> operation) { - std::mutex mutex; - std::condition_variable cv; - bool done = false; - Result result = Result(Error(-1, "Not completed")); - - operation(*dispatcher_, [&](Result r) { - std::lock_guard lock(mutex); - result = std::move(r); - done = true; - cv.notify_one(); - }); - - // Run dispatcher until done - while (true) { - { - std::unique_lock lock(mutex); - if (done) - break; - } - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - return result; - } - - std::unique_ptr dispatcher_; -}; diff --git a/third_party/gopher-orch/tests/gopher/orch/parallel_test.cc b/third_party/gopher-orch/tests/gopher/orch/parallel_test.cc deleted file mode 100644 index 9434cade..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/parallel_test.cc +++ /dev/null @@ -1,84 +0,0 @@ -// Unit tests for Parallel composition pattern - -#include "orch_test_fixture.h" - -// ============================================================================= -// Parallel Tests -// ============================================================================= - -TEST_F(OrchTest, ParallelBasic) { - auto branchA = makeJsonLambda( - [](const JsonValue& input) -> Result { - JsonValue result = JsonValue::object(); - result["a_result"] = JsonValue(input["value"].getInt() + 1); - return makeSuccess(JsonValue(result)); - }, - "BranchA"); - - auto branchB = makeJsonLambda( - [](const JsonValue& input) -> Result { - JsonValue result = JsonValue::object(); - result["b_result"] = JsonValue(input["value"].getInt() * 2); - return makeSuccess(JsonValue(result)); - }, - "BranchB"); - - auto par = - parallel("TestParallel").add("a", branchA).add("b", branchB).build(); - - EXPECT_EQ(par->size(), 2u); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - JsonValue input = JsonValue::object(); - input["value"] = JsonValue(10); - par->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // Check both branches executed - EXPECT_EQ(result["a"]["a_result"].getInt(), 11); // 10 + 1 - EXPECT_EQ(result["b"]["b_result"].getInt(), 20); // 10 * 2 -} - -TEST_F(OrchTest, ParallelFailFast) { - std::atomic branchB_completed{0}; - - auto branchA = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result( - Error(OrchError::INTERNAL_ERROR, "Branch A failed")); - }, - "FailingBranch"); - - auto branchB = makeJsonLambda( - [&branchB_completed](const JsonValue&) -> Result { - branchB_completed++; - JsonValue result = JsonValue::object(); - result["ok"] = JsonValue(true); - return makeSuccess(JsonValue(result)); - }, - "BranchB"); - - auto par = parallel().add("a", branchA).add("b", branchB).build(); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - par->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "Branch A failed"); - // Note: branchB may or may not complete depending on timing -} - -TEST_F(OrchTest, ParallelEmpty) { - auto par = parallel().build(); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - par->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - // Empty parallel returns empty object - EXPECT_TRUE(result.isObject()); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/rest_server_test.cc b/third_party/gopher-orch/tests/gopher/orch/rest_server_test.cc deleted file mode 100644 index f054fe9e..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/rest_server_test.cc +++ /dev/null @@ -1,517 +0,0 @@ -// Unit tests for RESTServer -// -// Tests REST server configuration, URL building, and integration with -// ServerComposite. Uses a mock HTTP client for isolated testing. - -#include "gopher/orch/server/rest_server.h" - -#include "orch_test_fixture.h" - -using namespace gopher::orch::server; - -// ============================================================================= -// Mock HTTP Client for Testing -// ============================================================================= - -class MockHttpClient : public HttpClient { - public: - struct RecordedRequest { - HttpMethod method; - std::string url; - std::map headers; - std::string body; - }; - - void request(HttpMethod method, - const std::string& url, - const std::map& headers, - const std::string& body, - Dispatcher& dispatcher, - ResponseCallback callback) override { - RecordedRequest req{method, url, headers, body}; - requests_.push_back(req); - - // Find matching response - HttpResponse response; - auto it = responses_.find(url); - if (it != responses_.end()) { - response = it->second; - } else if (default_response_.status_code != 0) { - response = default_response_; - } else { - response.status_code = 200; - response.body = "{}"; - } - - dispatcher.post( - [callback, response]() { callback(Result(response)); }); - } - - // Set response for a specific URL - void setResponse(const std::string& url, const HttpResponse& response) { - responses_[url] = response; - } - - // Set default response for any URL - void setDefaultResponse(const HttpResponse& response) { - default_response_ = response; - } - - // Set error response - void setError(const std::string& url, const Error& error) { - error_ = error; - error_url_ = url; - } - - // Get recorded requests - const std::vector& requests() const { return requests_; } - - // Clear recorded requests - void clearRequests() { requests_.clear(); } - - private: - std::vector requests_; - std::map responses_; - HttpResponse default_response_; - Error error_; - std::string error_url_; -}; - -// ============================================================================= -// RESTServer Configuration Tests -// ============================================================================= - -TEST_F(OrchTest, RESTServerConfigDefaults) { - RESTServerConfig config; - config.name = "test-api"; - config.base_url = "https://api.example.com/v1"; - - EXPECT_EQ(config.name, "test-api"); - EXPECT_EQ(config.base_url, "https://api.example.com/v1"); - EXPECT_EQ(config.auth.type, RESTServerConfig::AuthConfig::Type::NONE); - EXPECT_EQ(config.connect_timeout.count(), 10000); - EXPECT_EQ(config.request_timeout.count(), 30000); - EXPECT_TRUE(config.verify_ssl); -} - -TEST_F(OrchTest, RESTServerConfigFluentAPI) { - RESTServerConfig config; - config.name = "fluent-api"; - ; - config.base_url = "https://api.example.com"; - - config.addTool("get_users", "GET", "/users", "Get all users") - .addTool("create_user", "POST", "/users", "Create a user") - .addTool("get_user", "GET", "/users/{id}", "Get user by ID") - .setHeader("X-Custom", "value") - .setBearerAuth("token123"); - - EXPECT_EQ(config.tools.size(), 3u); - EXPECT_TRUE(config.tools.count("get_users") > 0); - EXPECT_TRUE(config.tools.count("create_user") > 0); - EXPECT_TRUE(config.tools.count("get_user") > 0); - - EXPECT_EQ(config.tools["get_users"].method, HttpMethod::GET); - EXPECT_EQ(config.tools["create_user"].method, HttpMethod::POST); - EXPECT_EQ(config.tools["get_user"].path, "/users/{id}"); - - EXPECT_EQ(config.default_headers["X-Custom"], "value"); - EXPECT_EQ(config.auth.type, RESTServerConfig::AuthConfig::Type::BEARER); - EXPECT_EQ(config.auth.bearer_token, "token123"); -} - -TEST_F(OrchTest, RESTServerConfigAuthTypes) { - RESTServerConfig config; - config.name = "auth-test"; - config.base_url = "https://api.example.com"; - - // Bearer auth - config.setBearerAuth("my-token"); - EXPECT_EQ(config.auth.type, RESTServerConfig::AuthConfig::Type::BEARER); - EXPECT_EQ(config.auth.bearer_token, "my-token"); - - // Basic auth - config.setBasicAuth("user", "pass"); - EXPECT_EQ(config.auth.type, RESTServerConfig::AuthConfig::Type::BASIC); - EXPECT_EQ(config.auth.username, "user"); - EXPECT_EQ(config.auth.password, "pass"); - - // API key auth - config.setApiKey("api-key-123", "X-API-Key"); - EXPECT_EQ(config.auth.type, RESTServerConfig::AuthConfig::Type::API_KEY); - EXPECT_EQ(config.auth.api_key, "api-key-123"); - EXPECT_EQ(config.auth.api_key_header, "X-API-Key"); -} - -// ============================================================================= -// RESTServer Creation Tests -// ============================================================================= - -TEST_F(OrchTest, RESTServerCreate) { - RESTServerConfig config; - config.name = "test-server"; - config.base_url = "https://api.example.com"; - config.addTool("test_tool", "GET", "/test"); - - auto mockClient = std::make_shared(); - auto server = RESTServer::create(config, mockClient); - - EXPECT_NE(server, nullptr); - EXPECT_EQ(server->name(), "test-server"); - EXPECT_EQ(server->connectionState(), ConnectionState::DISCONNECTED); -} - -TEST_F(OrchTest, RESTServerConnect) { - RESTServerConfig config; - config.name = "connect-test"; - config.base_url = "https://api.example.com"; - - auto mockClient = std::make_shared(); - auto server = RESTServer::create(config, mockClient); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - EXPECT_EQ(server->connectionState(), ConnectionState::CONNECTED); -} - -TEST_F(OrchTest, RESTServerConnectFailsWithoutBaseUrl) { - RESTServerConfig config; - config.name = "no-base-url"; - // base_url not set - - auto mockClient = std::make_shared(); - auto server = RESTServer::create(config, mockClient); - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); -} - -TEST_F(OrchTest, RESTServerListTools) { - RESTServerConfig config; - config.name = "list-tools-test"; - config.base_url = "https://api.example.com"; - config.addTool("tool1", "GET", "/t1", "Tool 1") - .addTool("tool2", "POST", "/t2", "Tool 2"); - - auto mockClient = std::make_shared(); - auto server = RESTServer::create(config, mockClient); - - // Connect first - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - auto tools = runToCompletion>( - [&](Dispatcher& d, ServerToolListCallback cb) { - server->listTools(d, std::move(cb)); - }); - - EXPECT_EQ(tools.size(), 2u); -} - -TEST_F(OrchTest, RESTServerGetTool) { - RESTServerConfig config; - config.name = "get-tool-test"; - config.base_url = "https://api.example.com"; - config.addTool("my_tool", "GET", "/my-endpoint"); - - auto mockClient = std::make_shared(); - auto server = RESTServer::create(config, mockClient); - - auto tool = server->tool("my_tool"); - EXPECT_NE(tool, nullptr); - EXPECT_EQ(tool->name(), "my_tool"); - - // Non-existent tool - auto missing = server->tool("nonexistent"); - EXPECT_EQ(missing, nullptr); -} - -TEST_F(OrchTest, RESTServerToolCaching) { - RESTServerConfig config; - config.name = "cache-test"; - config.base_url = "https://api.example.com"; - config.addTool("cached_tool", "GET", "/cached"); - - auto mockClient = std::make_shared(); - auto server = RESTServer::create(config, mockClient); - - auto tool1 = server->tool("cached_tool"); - auto tool2 = server->tool("cached_tool"); - - // Should return same cached instance - EXPECT_EQ(tool1.get(), tool2.get()); -} - -// ============================================================================= -// RESTServer Tool Invocation Tests -// ============================================================================= - -TEST_F(OrchTest, RESTServerCallToolGet) { - RESTServerConfig config; - config.name = "call-test"; - config.base_url = "http://localhost:8080"; - config.addTool("get_data", "GET", "/data"); - - auto mockClient = std::make_shared(); - HttpResponse mockResponse; - mockResponse.status_code = 200; - mockResponse.body = R"({"result": "success"})"; - mockClient->setDefaultResponse(mockResponse); - - auto server = RESTServer::create(config, mockClient); - - // Connect - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - // Call tool - JsonValue input = JsonValue::object(); - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - server->callTool("get_data", input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["result"].getString(), "success"); - - // Verify request was made - EXPECT_EQ(mockClient->requests().size(), 1u); - EXPECT_EQ(mockClient->requests()[0].method, HttpMethod::GET); - EXPECT_EQ(mockClient->requests()[0].url, "http://localhost:8080/data"); -} - -TEST_F(OrchTest, RESTServerCallToolPost) { - RESTServerConfig config; - config.name = "post-test"; - config.base_url = "http://localhost:8080"; - config.addTool("create_item", "POST", "/items"); - - auto mockClient = std::make_shared(); - HttpResponse mockResponse; - mockResponse.status_code = 201; - mockResponse.body = R"({"id": 123})"; - mockClient->setDefaultResponse(mockResponse); - - auto server = RESTServer::create(config, mockClient); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - JsonValue input = JsonValue::object(); - input["name"] = JsonValue("test item"); - - JsonValue result = runToCompletion([&](Dispatcher& d, - JsonCallback cb) { - server->callTool("create_item", input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["id"].getInt(), 123); - - // Verify request - EXPECT_EQ(mockClient->requests()[0].method, HttpMethod::POST); - EXPECT_EQ(mockClient->requests()[0].headers.at("Content-Type"), - "application/json"); - EXPECT_FALSE(mockClient->requests()[0].body.empty()); -} - -TEST_F(OrchTest, RESTServerCallToolWithPathParams) { - RESTServerConfig config; - config.name = "path-params-test"; - config.base_url = "http://localhost:8080"; - config.addTool("get_user", "GET", "/users/{user_id}/posts/{post_id}"); - - auto mockClient = std::make_shared(); - HttpResponse mockResponse; - mockResponse.status_code = 200; - mockResponse.body = R"({"title": "Hello"})"; - mockClient->setDefaultResponse(mockResponse); - - auto server = RESTServer::create(config, mockClient); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - JsonValue input = JsonValue::object(); - input["user_id"] = JsonValue("42"); - input["post_id"] = JsonValue(123); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - server->callTool("get_user", input, RunnableConfig(), d, std::move(cb)); - }); - - // Verify URL with substituted path parameters - EXPECT_EQ(mockClient->requests()[0].url, - "http://localhost:8080/users/42/posts/123"); -} - -TEST_F(OrchTest, RESTServerCallToolNotFound) { - RESTServerConfig config; - config.name = "not-found-test"; - config.base_url = "http://localhost:8080"; - config.addTool("existing_tool", "GET", "/exists"); - - auto mockClient = std::make_shared(); - auto server = RESTServer::create(config, mockClient); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - server->callTool("nonexistent_tool", JsonValue::object(), - RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); -} - -TEST_F(OrchTest, RESTServerCallToolHttpError) { - RESTServerConfig config; - config.name = "http-error-test"; - config.base_url = "http://localhost:8080"; - config.addTool("error_tool", "GET", "/error"); - - auto mockClient = std::make_shared(); - HttpResponse mockResponse; - mockResponse.status_code = 500; - mockResponse.body = "Internal Server Error"; - mockClient->setDefaultResponse(mockResponse); - - auto server = RESTServer::create(config, mockClient); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - server->callTool("error_tool", JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); -} - -// ============================================================================= -// RESTServer Authentication Tests -// ============================================================================= - -TEST_F(OrchTest, RESTServerBearerAuth) { - RESTServerConfig config; - config.name = "bearer-auth-test"; - config.base_url = "http://localhost:8080"; - config.setBearerAuth("my-secret-token"); - config.addTool("auth_tool", "GET", "/protected"); - - auto mockClient = std::make_shared(); - HttpResponse mockResponse; - mockResponse.status_code = 200; - mockResponse.body = "{}"; - mockClient->setDefaultResponse(mockResponse); - - auto server = RESTServer::create(config, mockClient); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - server->callTool("auth_tool", JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - // Verify Authorization header - EXPECT_EQ(mockClient->requests()[0].headers.at("Authorization"), - "Bearer my-secret-token"); -} - -TEST_F(OrchTest, RESTServerApiKeyAuth) { - RESTServerConfig config; - config.name = "api-key-test"; - config.base_url = "http://localhost:8080"; - config.setApiKey("secret-api-key", "X-API-Key"); - config.addTool("api_tool", "GET", "/api"); - - auto mockClient = std::make_shared(); - HttpResponse mockResponse; - mockResponse.status_code = 200; - mockResponse.body = "{}"; - mockClient->setDefaultResponse(mockResponse); - - auto server = RESTServer::create(config, mockClient); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - server->connect(d, std::move(cb)); - }); - - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - server->callTool("api_tool", JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - // Verify API key header - EXPECT_EQ(mockClient->requests()[0].headers.at("X-API-Key"), - "secret-api-key"); -} - -// ============================================================================= -// RESTServer with ServerComposite Tests -// ============================================================================= - -TEST_F(OrchTest, RESTServerWithComposite) { - RESTServerConfig config; - config.name = "rest-api"; - config.base_url = "http://localhost:8080"; - config.addTool("get_items", "GET", "/items"); - - auto mockClient = std::make_shared(); - HttpResponse mockResponse; - mockResponse.status_code = 200; - mockResponse.body = R"({"items": [1, 2, 3]})"; - mockClient->setDefaultResponse(mockResponse); - - auto restServer = RESTServer::create(config, mockClient); - - // Create composite with REST server - auto composite = ServerComposite::create("multi-server"); - std::vector tools = {"get_items"}; - composite->addServer(restServer, tools, true); - - // Connect all - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - composite->connectAll(d, std::move(cb)); - }); - - // Get tool through composite - auto tool = composite->tool("rest-api.get_items"); - EXPECT_NE(tool, nullptr); - - // Invoke through composite - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - tool->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result.contains("items")); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/retry_test.cc b/third_party/gopher-orch/tests/gopher/orch/retry_test.cc deleted file mode 100644 index ee07359a..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/retry_test.cc +++ /dev/null @@ -1,85 +0,0 @@ -// Unit tests for Retry resilience pattern - -#include "orch_test_fixture.h" - -// ============================================================================= -// Retry Tests -// ============================================================================= - -TEST_F(OrchTest, RetrySuccess) { - // Test that successful operation returns immediately - auto successLambda = makeJsonLambda( - [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["success"] = JsonValue(true); - return makeSuccess(JsonValue(result)); - }, - "SuccessLambda"); - - auto retryLambda = withRetry(successLambda, RetryPolicy::exponential(3)); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - retryLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_TRUE(result["success"].getBool()); -} - -TEST_F(OrchTest, RetryEventualSuccess) { - // Test that retry succeeds after failures - std::atomic attempt_count{0}; - - auto eventualSuccess = makeJsonLambda( - [&attempt_count](const JsonValue&) -> Result { - int attempt = ++attempt_count; - if (attempt < 3) { - return Result( - Error(OrchError::INTERNAL_ERROR, "Temporary failure")); - } - JsonValue result = JsonValue::object(); - result["attempt"] = JsonValue(attempt); - return makeSuccess(JsonValue(result)); - }, - "EventualSuccess"); - - // Use fixed delay policy for faster test - auto policy = RetryPolicy::fixed(5, 10); // 5 attempts, 10ms delay - auto retryLambda = withRetry(eventualSuccess, policy); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - retryLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_EQ(result["attempt"].getInt(), 3); - EXPECT_EQ(attempt_count.load(), 3); -} - -TEST_F(OrchTest, RetryExhausted) { - // Test that retry fails after max attempts - std::atomic attempt_count{0}; - - auto alwaysFails = makeJsonLambda( - [&attempt_count](const JsonValue&) -> Result { - attempt_count++; - return Result( - Error(OrchError::INTERNAL_ERROR, "Persistent failure")); - }, - "AlwaysFails"); - - auto policy = RetryPolicy::fixed(3, 10); // 3 attempts, 10ms delay - auto retryLambda = withRetry(alwaysFails, policy); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - retryLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "Persistent failure"); - EXPECT_EQ(attempt_count.load(), 3); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/router_test.cc b/third_party/gopher-orch/tests/gopher/orch/router_test.cc deleted file mode 100644 index a7fbb128..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/router_test.cc +++ /dev/null @@ -1,110 +0,0 @@ -// Unit tests for Router composition pattern - -#include "orch_test_fixture.h" - -// ============================================================================= -// Router Tests -// ============================================================================= - -TEST_F(OrchTest, RouterBasic) { - // Create branches for different conditions - auto positiveHandler = makeJsonLambda( - [](const JsonValue& input) -> Result { - JsonValue result = JsonValue::object(); - result["type"] = JsonValue("positive"); - result["value"] = JsonValue(input["value"].getInt()); - return makeSuccess(JsonValue(result)); - }, - "PositiveHandler"); - - auto negativeHandler = makeJsonLambda( - [](const JsonValue& input) -> Result { - JsonValue result = JsonValue::object(); - result["type"] = JsonValue("negative"); - result["value"] = JsonValue(input["value"].getInt()); - return makeSuccess(JsonValue(result)); - }, - "NegativeHandler"); - - auto defaultHandler = makeJsonLambda( - [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["type"] = JsonValue("zero"); - return makeSuccess(JsonValue(result)); - }, - "DefaultHandler"); - - auto routerRunnable = router("NumberRouter") - .when( - [](const JsonValue& input) { - return input["value"].getInt() > 0; - }, - positiveHandler) - .when( - [](const JsonValue& input) { - return input["value"].getInt() < 0; - }, - negativeHandler) - .otherwise(defaultHandler) - .build(); - - // Test positive number - JsonValue positiveInput = JsonValue::object(); - positiveInput["value"] = JsonValue(42); - - JsonValue result1 = runToCompletion([&](Dispatcher& d, - JsonCallback cb) { - routerRunnable->invoke(positiveInput, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result1["type"].getString(), "positive"); - EXPECT_EQ(result1["value"].getInt(), 42); - - // Test negative number - JsonValue negativeInput = JsonValue::object(); - negativeInput["value"] = JsonValue(-10); - - JsonValue result2 = runToCompletion([&](Dispatcher& d, - JsonCallback cb) { - routerRunnable->invoke(negativeInput, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result2["type"].getString(), "negative"); - - // Test zero (default) - JsonValue zeroInput = JsonValue::object(); - zeroInput["value"] = JsonValue(0); - - JsonValue result3 = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - routerRunnable->invoke(zeroInput, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result3["type"].getString(), "zero"); -} - -TEST_F(OrchTest, RouterNoMatchNoDefault) { - // Router without default route should return error - auto handler = makeJsonLambda( - [](const JsonValue&) -> Result { - return makeSuccess(JsonValue::object()); - }, - "Handler"); - - auto routerRunnable = - router() - .when([](const JsonValue& input) { return input["match"].getBool(); }, - handler) - .build(); - - JsonValue input = JsonValue::object(); - input["match"] = JsonValue(false); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - routerRunnable->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::INVALID_ARGUMENT); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/sequence_test.cc b/third_party/gopher-orch/tests/gopher/orch/sequence_test.cc deleted file mode 100644 index 5f792473..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/sequence_test.cc +++ /dev/null @@ -1,87 +0,0 @@ -// Unit tests for Sequence composition pattern - -#include "orch_test_fixture.h" - -// ============================================================================= -// Sequence Tests -// ============================================================================= - -TEST_F(OrchTest, SequenceBasic) { - // Create two lambdas and chain them - auto step1 = makeJsonLambda( - [](const JsonValue& input) -> Result { - JsonValue result = JsonValue::object(); - result["step1"] = JsonValue(true); - result["value"] = JsonValue(input["value"].getInt() + 1); - return makeSuccess(JsonValue(result)); - }, - "Step1"); - - auto step2 = makeJsonLambda( - [](const JsonValue& input) -> Result { - JsonValue result = JsonValue::object(); - result["step2"] = JsonValue(true); - result["value"] = JsonValue(input["value"].getInt() * 2); - return makeSuccess(JsonValue(result)); - }, - "Step2"); - - auto seq = sequence("TestSequence").add(step1).add(step2).build(); - - EXPECT_EQ(seq->size(), 2u); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - JsonValue input = JsonValue::object(); - input["value"] = JsonValue(10); - seq->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // (10 + 1) * 2 = 22 - EXPECT_EQ(result["value"].getInt(), 22); - EXPECT_TRUE(result["step2"].getBool()); -} - -TEST_F(OrchTest, SequenceShortCircuit) { - std::atomic step2_called{0}; - - auto step1 = makeJsonLambda( - [](const JsonValue&) -> Result { - return Result( - Error(OrchError::INVALID_ARGUMENT, "Step1 failed")); - }, - "FailingStep"); - - auto step2 = makeJsonLambda( - [&step2_called](const JsonValue& input) -> Result { - step2_called++; - return makeSuccess(JsonValue(input)); - }, - "Step2"); - - auto seq = sequence().add(step1).add(step2).build(); - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - seq->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "Step1 failed"); - EXPECT_EQ(step2_called.load(), 0); // Step2 should not be called -} - -TEST_F(OrchTest, SequenceEmpty) { - auto seq = sequence().build(); - - JsonValue input = JsonValue::object(); - input["pass_through"] = JsonValue(true); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - seq->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // Empty sequence passes through input - EXPECT_TRUE(result["pass_through"].getBool()); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/server_composite_test.cc b/third_party/gopher-orch/tests/gopher/orch/server_composite_test.cc deleted file mode 100644 index 07095fcb..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/server_composite_test.cc +++ /dev/null @@ -1,372 +0,0 @@ -// Unit tests for ServerComposite -// -// Tests multi-server aggregation, tool namespacing, aliasing, -// and connection management across multiple mock servers. - -#include "orch_test_fixture.h" - -// ============================================================================= -// ServerComposite Tests -// ============================================================================= - -TEST_F(OrchTest, ServerCompositeCreate) { - auto composite = ServerComposite::create("test-composite"); - EXPECT_EQ(composite->name(), "test-composite"); - EXPECT_TRUE(composite->listTools().empty()); - EXPECT_TRUE(composite->servers().empty()); -} - -TEST_F(OrchTest, ServerCompositeAddServer) { - auto composite = ServerComposite::create("test-composite"); - - auto server1 = makeMockServer("server1"); - server1->addTool("tool1", "First tool"); - - auto server2 = makeMockServer("server2"); - server2->addTool("tool2", "Second tool"); - - // Add servers with explicit tool mappings - std::vector tools1 = {"tool1"}; - std::vector tools2 = {"tool2"}; - composite->addServer(server1, tools1, true); - composite->addServer(server2, tools2, true); - - EXPECT_EQ(composite->servers().size(), 2u); - EXPECT_NE(composite->server("server1"), nullptr); - EXPECT_NE(composite->server("server2"), nullptr); - EXPECT_EQ(composite->server("nonexistent"), nullptr); -} - -TEST_F(OrchTest, ServerCompositeToolNamespacing) { - // Tests that tools are namespaced by server name when namespace_tools=true - auto composite = ServerComposite::create("namespaced"); - - auto server = makeMockServer("weather"); - server->addTool("get_forecast", "Gets weather forecast"); - server->setResponse("get_forecast", JsonValue("Sunny")); - - std::vector tool_names = {"get_forecast"}; - composite->addServer(server, tool_names, true); - - // Tools should be listed with namespace prefix - auto tools = composite->listTools(); - EXPECT_EQ(tools.size(), 1u); - EXPECT_EQ(tools[0], "weather.get_forecast"); - - // Can get tool by fully-qualified name - EXPECT_TRUE(composite->hasTool("weather.get_forecast")); -} - -TEST_F(OrchTest, ServerCompositeNoNamespacing) { - // Tests that tools are exposed without prefix when namespace_tools=false - auto composite = ServerComposite::create("flat"); - - auto server = makeMockServer("myserver"); - server->addTool("simple_tool", "A simple tool"); - - std::vector tool_names = {"simple_tool"}; - composite->addServer(server, tool_names, false); - - auto tools = composite->listTools(); - EXPECT_EQ(tools.size(), 1u); - EXPECT_EQ(tools[0], "simple_tool"); - - EXPECT_TRUE(composite->hasTool("simple_tool")); -} - -TEST_F(OrchTest, ServerCompositeAliases) { - // Tests tool aliasing - expose tools under different names - auto composite = ServerComposite::create("aliased"); - - auto server = makeMockServer("complex-name-server"); - server->addTool("internal_get_data_v2", "Gets data"); - server->setResponse("internal_get_data_v2", JsonValue("data")); - - // Map internal name to a simpler alias - std::map aliases = { - {"get_data", "internal_get_data_v2"}, {"fetch", "internal_get_data_v2"} - // Multiple aliases for same tool - }; - composite->addServerWithAliases(server, aliases); - - auto tools = composite->listTools(); - EXPECT_EQ(tools.size(), 2u); - - EXPECT_TRUE(composite->hasTool("get_data")); - EXPECT_TRUE(composite->hasTool("fetch")); -} - -TEST_F(OrchTest, ServerCompositeAddSingleTool) { - // Tests adding individual tools with optional alias - auto composite = ServerComposite::create("single-tool"); - - auto server = makeMockServer("myserver"); - server->addTool("tool1", "Tool one"); - server->addTool("tool2", "Tool two"); - - // Add only one tool with an alias - composite->addTool(server, "tool1", "my_tool"); - - auto tools = composite->listTools(); - EXPECT_EQ(tools.size(), 1u); - EXPECT_EQ(tools[0], "my_tool"); - - EXPECT_TRUE(composite->hasTool("my_tool")); - EXPECT_FALSE(composite->hasTool("tool1")); - EXPECT_FALSE(composite->hasTool("tool2")); -} - -TEST_F(OrchTest, ServerCompositeRemoveServer) { - auto composite = ServerComposite::create("removable"); - - auto server1 = makeMockServer("server1"); - server1->addTool("tool1"); - std::vector t1 = {"tool1"}; - composite->addServer(server1, t1, true); - - auto server2 = makeMockServer("server2"); - server2->addTool("tool2"); - std::vector t2 = {"tool2"}; - composite->addServer(server2, t2, true); - - EXPECT_EQ(composite->servers().size(), 2u); - EXPECT_TRUE(composite->hasTool("server1.tool1")); - EXPECT_TRUE(composite->hasTool("server2.tool2")); - - // Remove server1 - composite->removeServer("server1"); - - EXPECT_EQ(composite->servers().size(), 1u); - EXPECT_FALSE(composite->hasTool("server1.tool1")); - EXPECT_TRUE(composite->hasTool("server2.tool2")); -} - -TEST_F(OrchTest, ServerCompositeConnectAll) { - // Tests connecting all servers at once - auto composite = ServerComposite::create("connect-all"); - - auto server1 = makeMockServer("server1"); - server1->addTool("tool1"); - composite->addServer(server1, std::vector{"tool1"}, true); - - auto server2 = makeMockServer("server2"); - server2->addTool("tool2"); - composite->addServer(server2, std::vector{"tool2"}, true); - - // Both servers should be disconnected initially - EXPECT_EQ(server1->connectionState(), ConnectionState::DISCONNECTED); - EXPECT_EQ(server2->connectionState(), ConnectionState::DISCONNECTED); - - // Connect all servers - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - composite->connectAll(d, std::move(cb)); - }); - - // Both servers should now be connected - EXPECT_TRUE(server1->isConnected()); - EXPECT_TRUE(server2->isConnected()); -} - -TEST_F(OrchTest, ServerCompositeConnectAllEmpty) { - // Tests connecting when no servers are added (should succeed immediately) - auto composite = ServerComposite::create("empty"); - - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - composite->connectAll(d, std::move(cb)); - }); - // Should complete without error -} - -TEST_F(OrchTest, ServerCompositeDisconnectAll) { - auto composite = ServerComposite::create("disconnect-all"); - - auto server1 = makeMockServer("server1"); - server1->addTool("tool1"); - std::vector t1 = {"tool1"}; - composite->addServer(server1, t1, true); - - auto server2 = makeMockServer("server2"); - server2->addTool("tool2"); - std::vector t2 = {"tool2"}; - composite->addServer(server2, t2, true); - - // Connect first - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - composite->connectAll(d, std::move(cb)); - }); - - EXPECT_TRUE(server1->isConnected()); - EXPECT_TRUE(server2->isConnected()); - - // Disconnect all - bool disconnected = false; - composite->disconnectAll(*dispatcher_, [&]() { disconnected = true; }); - - // Run dispatcher until callback fires - while (!disconnected) { - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - EXPECT_FALSE(server1->isConnected()); - EXPECT_FALSE(server2->isConnected()); -} - -TEST_F(OrchTest, ServerCompositeToolInvocation) { - // Tests invoking a tool through the composite - auto composite = ServerComposite::create("invoke-test"); - - auto server = makeMockServer("math"); - server->addTool("add", "Adds two numbers"); - server->setHandler("add", [](const JsonValue& args) -> Result { - int a = args["a"].getInt(); - int b = args["b"].getInt(); - JsonValue result = JsonValue::object(); - result["sum"] = JsonValue(a + b); - return makeSuccess(JsonValue(result)); - }); - - std::vector tool_names = {"add"}; - composite->addServer(server, tool_names, true); - - // Connect - runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - composite->connectAll(d, std::move(cb)); - }); - - // Get tool through composite - auto addTool = composite->tool("math.add"); - EXPECT_NE(addTool, nullptr); - EXPECT_EQ(addTool->name(), "math.add"); - - // Invoke the tool - JsonValue input = JsonValue::object(); - input["a"] = JsonValue(3); - input["b"] = JsonValue(5); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - addTool->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["sum"].getInt(), 8); -} - -TEST_F(OrchTest, ServerCompositeToolByServerAndName) { - // Tests the two-argument tool() method - auto composite = ServerComposite::create("two-arg"); - - auto server = makeMockServer("myserver"); - server->addTool("mytool"); - server->setResponse("mytool", JsonValue("result")); - - std::vector tool_names = {"mytool"}; - composite->addServer(server, tool_names, true); - - // Get tool using server name and tool name - auto tool = composite->tool("myserver", "mytool"); - EXPECT_NE(tool, nullptr); -} - -TEST_F(OrchTest, ServerCompositeToolCaching) { - // Tests that tool objects are cached - auto composite = ServerComposite::create("cache-test"); - - auto server = makeMockServer("server"); - server->addTool("tool"); - std::vector tool_names = {"tool"}; - composite->addServer(server, tool_names, true); - - auto tool1 = composite->tool("server.tool"); - auto tool2 = composite->tool("server.tool"); - - // Should return the same cached object - EXPECT_EQ(tool1.get(), tool2.get()); -} - -TEST_F(OrchTest, ServerCompositeToolNotFound) { - auto composite = ServerComposite::create("not-found"); - - auto server = makeMockServer("server"); - server->addTool("existing_tool"); - std::vector tool_names = {"existing_tool"}; - composite->addServer(server, tool_names, true); - - // Try to get a non-existent tool by alias/direct name - returns nullptr - auto tool = composite->tool("nonexistent"); - EXPECT_EQ(tool, nullptr); - - EXPECT_FALSE(composite->hasTool("nonexistent")); - - // Note: Fully-qualified names (server.tool) can resolve to any tool on - // a registered server, even if not explicitly mapped. This allows dynamic - // tool discovery while still supporting explicit mappings for aliases. - EXPECT_TRUE(composite->hasTool("server.existing_tool")); // Mapped explicitly -} - -TEST_F(OrchTest, ServerCompositeMultipleToolsSameServer) { - // Tests adding multiple tools from the same server - auto composite = ServerComposite::create("multi-tool"); - - auto server = makeMockServer("api"); - server->addTool("read", "Reads data"); - server->addTool("write", "Writes data"); - server->addTool("delete", "Deletes data"); - - std::vector tool_names = {"read", "write", "delete"}; - composite->addServer(server, tool_names, true); - - auto tools = composite->listTools(); - EXPECT_EQ(tools.size(), 3u); - - EXPECT_TRUE(composite->hasTool("api.read")); - EXPECT_TRUE(composite->hasTool("api.write")); - EXPECT_TRUE(composite->hasTool("api.delete")); -} - -TEST_F(OrchTest, ServerCompositeListToolInfos) { - auto composite = ServerComposite::create("info-test"); - - auto server = makeMockServer("server"); - server->addTool("tool1", "Tool one description"); - server->addTool("tool2", "Tool two description"); - - std::vector tool_names = {"tool1", "tool2"}; - composite->addServer(server, tool_names, true); - - auto infos = composite->listToolInfos(); - EXPECT_EQ(infos.size(), 2u); - - // Check that exposed names are set - bool found_tool1 = false, found_tool2 = false; - for (const auto& info : infos) { - if (info.name == "server.tool1") - found_tool1 = true; - if (info.name == "server.tool2") - found_tool2 = true; - } - EXPECT_TRUE(found_tool1); - EXPECT_TRUE(found_tool2); -} - -TEST_F(OrchTest, ServerCompositeChainedAdditions) { - // Tests fluent API for adding servers and tools - auto composite = ServerComposite::create("chained"); - - auto server1 = makeMockServer("s1"); - server1->addTool("t1"); - auto server2 = makeMockServer("s2"); - server2->addTool("t2"); - - // Chain additions - std::vector t1 = {"t1"}; - std::vector t2 = {"t2"}; - composite->addServer(server1, t1, true).addServer(server2, t2, true); - - EXPECT_EQ(composite->servers().size(), 2u); - EXPECT_EQ(composite->listTools().size(), 2u); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/state_graph_test.cc b/third_party/gopher-orch/tests/gopher/orch/state_graph_test.cc deleted file mode 100644 index be84ae3b..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/state_graph_test.cc +++ /dev/null @@ -1,376 +0,0 @@ -// Unit tests for StateGraph (stateful workflow graphs) - -#include "orch_test_fixture.h" - -using namespace gopher::orch::graph; - -// ============================================================================= -// StateGraph Tests -// ============================================================================= - -TEST_F(OrchTest, StateGraphBasic) { - // Create a simple linear graph: start -> process -> end - StateGraph graph; - graph - .addNode("start", - [](const GraphState& state) { - GraphState result = state; - result.set("step", JsonValue("started")); - return result; - }) - .addNode("process", - [](const GraphState& state) { - GraphState result = state; - result.set("step", JsonValue("processed")); - result.set("value", - JsonValue(state.get("input").getInt() * 2)); - return result; - }) - .addEdge("start", "process") - .addEdge("process", StateGraph::END()) - .setEntryPoint("start"); - - auto compiled = graph.compile(); - - JsonValue input = JsonValue::object(); - input["input"] = JsonValue(21); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - compiled->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["step"].getString(), "processed"); - EXPECT_EQ(result["value"].getInt(), 42); -} - -TEST_F(OrchTest, StateGraphConditionalEdge) { - // Create a graph with conditional branching - StateGraph graph; - graph - .addNode("check", - [](const GraphState& state) { - // Just pass through - condition is evaluated on edge - return state; - }) - .addNode("positive_path", - [](const GraphState& state) { - GraphState result = state; - result.set("path", JsonValue("positive")); - return result; - }) - .addNode("negative_path", - [](const GraphState& state) { - GraphState result = state; - result.set("path", JsonValue("negative")); - return result; - }) - .addConditionalEdge("check", - [](const GraphState& state) { - int value = state.get("value").getInt(); - if (value > 0) { - return std::string("positive_path"); - } else { - return std::string("negative_path"); - } - }) - .addEdge("positive_path", StateGraph::END()) - .addEdge("negative_path", StateGraph::END()) - .setEntryPoint("check"); - - auto compiled = graph.compile(); - - // Test positive path - JsonValue positiveInput = JsonValue::object(); - positiveInput["value"] = JsonValue(10); - - JsonValue result1 = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - compiled->invoke(positiveInput, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result1["path"].getString(), "positive"); - - // Test negative path - JsonValue negativeInput = JsonValue::object(); - negativeInput["value"] = JsonValue(-5); - - JsonValue result2 = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - compiled->invoke(negativeInput, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result2["path"].getString(), "negative"); -} - -TEST_F(OrchTest, StateGraphWithRunnable) { - // Create a graph using JsonRunnable nodes - auto doubler = makeJsonLambda( - [](const JsonValue& input) -> Result { - JsonValue result = JsonValue::object(); - result["doubled"] = JsonValue(input["value"].getInt() * 2); - return makeSuccess(result); - }, - "Doubler"); - - StateGraph graph; - graph.addNode("double", doubler) - .addEdge("double", StateGraph::END()) - .setEntryPoint("double"); - - auto compiled = graph.compile(); - - JsonValue input = JsonValue::object(); - input["value"] = JsonValue(21); - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - compiled->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["doubled"].getInt(), 42); - EXPECT_EQ(result["value"].getInt(), 21); // Original value preserved -} - -TEST_F(OrchTest, StateGraphNoEntryPoint) { - StateGraph graph; - graph.addNode("node", [](const GraphState& state) { return state; }); - - auto compiled = graph.compile(); - - auto result = runToCompletionResult([&](Dispatcher& d, - JsonCallback cb) { - compiled->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::INVALID_ARGUMENT); -} - -TEST_F(OrchTest, StateGraphNodeNotFound) { - StateGraph graph; - graph.setEntryPoint("nonexistent"); - - auto compiled = graph.compile(); - - auto result = runToCompletionResult([&](Dispatcher& d, - JsonCallback cb) { - compiled->invoke(JsonValue::object(), RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::INVALID_ARGUMENT); -} - -TEST_F(OrchTest, GraphStateOperations) { - GraphState state; - - // Test set/get - state.set("key1", JsonValue("value1")); - state.set("key2", JsonValue(42)); - - EXPECT_TRUE(state.has("key1")); - EXPECT_TRUE(state.has("key2")); - EXPECT_FALSE(state.has("key3")); - - EXPECT_EQ(state.get("key1").getString(), "value1"); - EXPECT_EQ(state.get("key2").getInt(), 42); - EXPECT_TRUE(state.get("key3").isNull()); - - // Test version tracking - EXPECT_EQ(state.version("key1"), 1u); - state.set("key1", JsonValue("updated")); - EXPECT_EQ(state.version("key1"), 2u); - - // Test JSON serialization - JsonValue json = state.toJson(); - EXPECT_EQ(json["key1"].getString(), "updated"); - EXPECT_EQ(json["key2"].getInt(), 42); - - // Test fromJson - GraphState restored = GraphState::fromJson(json); - EXPECT_EQ(restored.get("key1").getString(), "updated"); - EXPECT_EQ(restored.get("key2").getInt(), 42); -} - -// ============================================================================= -// GraphState Channel/Reducer Tests -// ============================================================================= - -TEST_F(OrchTest, GraphStateWithReducerAppendArray) { - GraphState state; - - // Configure channel with array append reducer - state.configureChannel("messages", reducers::appendArray); - - // First message - JsonValue msg1 = JsonValue::array(); - msg1.push_back(JsonValue("hello")); - state.set("messages", msg1); - EXPECT_EQ(state.get("messages").size(), 1u); - EXPECT_EQ(state.get("messages")[0].getString(), "hello"); - - // Second message should be appended - JsonValue msg2 = JsonValue::array(); - msg2.push_back(JsonValue("world")); - state.set("messages", msg2); - EXPECT_EQ(state.get("messages").size(), 2u); - EXPECT_EQ(state.get("messages")[0].getString(), "hello"); - EXPECT_EQ(state.get("messages")[1].getString(), "world"); - - // Third message - JsonValue msg3 = JsonValue::array(); - msg3.push_back(JsonValue("!")); - state.set("messages", msg3); - EXPECT_EQ(state.get("messages").size(), 3u); -} - -TEST_F(OrchTest, GraphStateWithReducerMergeObjects) { - GraphState state; - - // Configure channel with object merge reducer - state.configureChannel("data", reducers::mergeObjects); - - // First object - JsonValue obj1 = JsonValue::object(); - obj1["a"] = JsonValue(1); - state.set("data", obj1); - EXPECT_EQ(state.get("data")["a"].getInt(), 1); - - // Second object should be merged - JsonValue obj2 = JsonValue::object(); - obj2["b"] = JsonValue(2); - state.set("data", obj2); - EXPECT_EQ(state.get("data")["a"].getInt(), 1); // preserved - EXPECT_EQ(state.get("data")["b"].getInt(), 2); // added - - // Third object should overwrite existing key - JsonValue obj3 = JsonValue::object(); - obj3["a"] = JsonValue(10); - obj3["c"] = JsonValue(3); - state.set("data", obj3); - EXPECT_EQ(state.get("data")["a"].getInt(), 10); // overwritten - EXPECT_EQ(state.get("data")["b"].getInt(), 2); // preserved - EXPECT_EQ(state.get("data")["c"].getInt(), 3); // added -} - -TEST_F(OrchTest, GraphStateWithCustomReducer) { - GraphState state; - - // Configure channel with custom max reducer - state.configureChannel( - "max_score", [](const JsonValue& old_val, const JsonValue& new_val) { - int old_score = old_val.getInt(); - int new_score = new_val.getInt(); - return JsonValue(std::max(old_score, new_score)); - }); - - state.set("max_score", JsonValue(10)); - EXPECT_EQ(state.get("max_score").getInt(), 10); - - state.set("max_score", JsonValue(5)); // Lower, should not change - EXPECT_EQ(state.get("max_score").getInt(), 10); - - state.set("max_score", JsonValue(20)); // Higher, should update - EXPECT_EQ(state.get("max_score").getInt(), 20); -} - -TEST_F(OrchTest, GraphStateMergeWithReducers) { - GraphState state1; - state1.configureChannel("items", reducers::appendArray); - - JsonValue items1 = JsonValue::array(); - items1.push_back(JsonValue(1)); - items1.push_back(JsonValue(2)); - state1.set("items", items1); - - GraphState state2; - JsonValue items2 = JsonValue::array(); - items2.push_back(JsonValue(3)); - state2.set("items", items2); - - // Merge should use reducer from state1 - state1.merge(state2); - EXPECT_EQ(state1.get("items").size(), 3u); - EXPECT_EQ(state1.get("items")[0].getInt(), 1); - EXPECT_EQ(state1.get("items")[1].getInt(), 2); - EXPECT_EQ(state1.get("items")[2].getInt(), 3); -} - -TEST_F(OrchTest, StateChannelTemplate) { - // Test the template version of StateChannel - StateChannel counter; - EXPECT_FALSE(counter.hasValue()); - EXPECT_EQ(counter.version(), 0u); - - counter.update(10); - EXPECT_TRUE(counter.hasValue()); - EXPECT_EQ(counter.value(), 10); - EXPECT_EQ(counter.version(), 1u); - - counter.update(20); - EXPECT_EQ(counter.value(), 20); // Last write wins (no reducer) - EXPECT_EQ(counter.version(), 2u); -} - -TEST_F(OrchTest, StateChannelWithReducer) { - // Test StateChannel with a custom reducer (sum) - StateChannel sum([](const int& a, const int& b) { return a + b; }); - - sum.update(10); - EXPECT_EQ(sum.value(), 10); - - sum.update(5); - EXPECT_EQ(sum.value(), 15); // 10 + 5 - - sum.update(3); - EXPECT_EQ(sum.value(), 18); // 15 + 3 -} - -TEST_F(OrchTest, GraphStateCopy) { - GraphState original; - original.configureChannel("data", reducers::appendArray); - - JsonValue arr = JsonValue::array(); - arr.push_back(JsonValue(1)); - original.set("data", arr); - - // Copy should preserve reducer configuration - GraphState copied = original.copy(); - - JsonValue arr2 = JsonValue::array(); - arr2.push_back(JsonValue(2)); - copied.set("data", arr2); - - // Original should be unchanged - EXPECT_EQ(original.get("data").size(), 1u); - - // Copied should have appended (reducer preserved) - EXPECT_EQ(copied.get("data").size(), 2u); -} - -TEST_F(OrchTest, GraphStateKeys) { - GraphState state; - state.set("alpha", JsonValue(1)); - state.set("beta", JsonValue(2)); - state.set("gamma", JsonValue(3)); - - auto keys = state.keys(); - EXPECT_EQ(keys.size(), 3u); - - // Keys should be sorted (std::map order) - EXPECT_EQ(keys[0], "alpha"); - EXPECT_EQ(keys[1], "beta"); - EXPECT_EQ(keys[2], "gamma"); -} - -TEST_F(OrchTest, StateGraphSTARTConstant) { - // Verify START() constant exists and is different from END() - EXPECT_EQ(StateGraph::START(), "__start__"); - EXPECT_EQ(StateGraph::END(), "__end__"); - EXPECT_NE(StateGraph::START(), StateGraph::END()); - - // Also verify on CompiledStateGraph - EXPECT_EQ(CompiledStateGraph::START(), "__start__"); - EXPECT_EQ(CompiledStateGraph::END(), "__end__"); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/state_machine_test.cc b/third_party/gopher-orch/tests/gopher/orch/state_machine_test.cc deleted file mode 100644 index a69357d1..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/state_machine_test.cc +++ /dev/null @@ -1,226 +0,0 @@ -// Unit tests for StateMachine (finite state machine) - -#include "orch_test_fixture.h" - -using namespace gopher::orch::fsm; - -// Define test states and events (prefixed with Test to avoid conflict with -// server::TestConnState) -enum class TestConnState { DISCONNECTED, CONNECTING, CONNECTED, ERROR }; -enum class TestConnEvent { CONNECT, CONNECTED, DISCONNECT, FAIL }; - -// ============================================================================= -// StateMachine Tests -// ============================================================================= - -TEST_F(OrchTest, StateMachineBasic) { - // Create a simple connection state machine - StateMachine sm(TestConnState::DISCONNECTED); - - sm.addTransition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING) - .addTransition(TestConnState::CONNECTING, TestConnEvent::CONNECTED, - TestConnState::CONNECTED) - .addTransition(TestConnState::CONNECTED, TestConnEvent::DISCONNECT, - TestConnState::DISCONNECTED) - .addTransition(TestConnState::CONNECTING, TestConnEvent::FAIL, - TestConnState::ERROR) - .addTransition(TestConnState::ERROR, TestConnEvent::CONNECT, - TestConnState::CONNECTING); - - EXPECT_EQ(sm.currentState(), TestConnState::DISCONNECTED); - - // Trigger transitions - auto result1 = sm.trigger(TestConnEvent::CONNECT); - EXPECT_TRUE(mcp::holds_alternative(result1)); - EXPECT_EQ(sm.currentState(), TestConnState::CONNECTING); - - auto result2 = sm.trigger(TestConnEvent::CONNECTED); - EXPECT_TRUE(mcp::holds_alternative(result2)); - EXPECT_EQ(sm.currentState(), TestConnState::CONNECTED); - - auto result3 = sm.trigger(TestConnEvent::DISCONNECT); - EXPECT_TRUE(mcp::holds_alternative(result3)); - EXPECT_EQ(sm.currentState(), TestConnState::DISCONNECTED); -} - -TEST_F(OrchTest, StateMachineInvalidTransition) { - StateMachine sm(TestConnState::DISCONNECTED); - - sm.addTransition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING); - - // Try invalid transition - auto result = sm.trigger(TestConnEvent::DISCONNECT); - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::INVALID_TRANSITION); - EXPECT_EQ(sm.currentState(), TestConnState::DISCONNECTED); -} - -TEST_F(OrchTest, StateMachineWithGuard) { - // Use int as context to track retry count - StateMachine sm( - TestConnState::DISCONNECTED); - - sm.addTransition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING) - .addTransition(TestConnState::CONNECTING, TestConnEvent::FAIL, - TestConnState::DISCONNECTED) - .setGuard(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - [](TestConnState, TestConnEvent, const int& retries) { - // Only allow connect if retries < 3 - return retries < 3; - }); - - sm.setContext(0); - - // First connect should work - auto result1 = sm.trigger(TestConnEvent::CONNECT); - EXPECT_TRUE(mcp::holds_alternative(result1)); - EXPECT_EQ(sm.currentState(), TestConnState::CONNECTING); - - // Fail and increment retry count - sm.trigger(TestConnEvent::FAIL); - sm.setContext(1); - - // Second connect should work - auto result2 = sm.trigger(TestConnEvent::CONNECT); - EXPECT_TRUE(mcp::holds_alternative(result2)); - - sm.trigger(TestConnEvent::FAIL); - sm.setContext(3); // Set to 3 retries - - // Third connect should be rejected by guard - auto result3 = sm.trigger(TestConnEvent::CONNECT); - EXPECT_TRUE(mcp::holds_alternative(result3)); - EXPECT_EQ(mcp::get(result3).code, OrchError::GUARD_REJECTED); -} - -TEST_F(OrchTest, StateMachineWithCallbacks) { - std::vector log; - - StateMachine sm(TestConnState::DISCONNECTED); - - sm.addTransition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING) - .addTransition(TestConnState::CONNECTING, TestConnEvent::CONNECTED, - TestConnState::CONNECTED) - .onEnter( - TestConnState::CONNECTING, - [&log](TestConnState, void*&) { log.push_back("enter_connecting"); }) - .onExit( - TestConnState::CONNECTING, - [&log](TestConnState, void*&) { log.push_back("exit_connecting"); }) - .onEnter( - TestConnState::CONNECTED, - [&log](TestConnState, void*&) { log.push_back("enter_connected"); }) - .onStateChange([&log](TestConnState from, TestConnState to, - TestConnEvent) { log.push_back("state_change"); }); - - sm.trigger(TestConnEvent::CONNECT); - sm.trigger(TestConnEvent::CONNECTED); - - EXPECT_EQ(log.size(), 5u); - EXPECT_EQ(log[0], "enter_connecting"); - EXPECT_EQ(log[1], "state_change"); - EXPECT_EQ(log[2], "exit_connecting"); - EXPECT_EQ(log[3], "enter_connected"); - EXPECT_EQ(log[4], "state_change"); -} - -TEST_F(OrchTest, StateMachineValidEvents) { - StateMachine sm(TestConnState::DISCONNECTED); - - sm.addTransition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING) - .addTransition(TestConnState::CONNECTING, TestConnEvent::CONNECTED, - TestConnState::CONNECTED) - .addTransition(TestConnState::CONNECTING, TestConnEvent::FAIL, - TestConnState::ERROR); - - // From DISCONNECTED, only CONNECT is valid - auto events = sm.validEvents(); - EXPECT_EQ(events.size(), 1u); - EXPECT_EQ(events[0], TestConnEvent::CONNECT); - - // Move to CONNECTING - sm.trigger(TestConnEvent::CONNECT); - - // From CONNECTING, CONNECTED and FAIL are valid - events = sm.validEvents(); - EXPECT_EQ(events.size(), 2u); -} - -TEST_F(OrchTest, StateMachineCanTrigger) { - StateMachine sm(TestConnState::DISCONNECTED); - - sm.addTransition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING); - - EXPECT_TRUE(sm.canTrigger(TestConnEvent::CONNECT)); - EXPECT_FALSE(sm.canTrigger(TestConnEvent::DISCONNECT)); - EXPECT_FALSE(sm.canTrigger(TestConnEvent::CONNECTED)); -} - -TEST_F(OrchTest, StateMachineBuilder) { - auto sm = makeStateMachine( - TestConnState::DISCONNECTED) - .transition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING) - .transition(TestConnState::CONNECTING, TestConnEvent::CONNECTED, - TestConnState::CONNECTED) - .build(); - - EXPECT_EQ(sm->currentState(), TestConnState::DISCONNECTED); - - sm->trigger(TestConnEvent::CONNECT); - EXPECT_EQ(sm->currentState(), TestConnState::CONNECTING); - - sm->trigger(TestConnEvent::CONNECTED); - EXPECT_EQ(sm->currentState(), TestConnState::CONNECTED); -} - -TEST_F(OrchTest, StateMachineReset) { - StateMachine sm(TestConnState::CONNECTED); - - EXPECT_EQ(sm.currentState(), TestConnState::CONNECTED); - - sm.reset(TestConnState::DISCONNECTED); - EXPECT_EQ(sm.currentState(), TestConnState::DISCONNECTED); -} - -TEST_F(OrchTest, StateMachineAsyncTrigger) { - StateMachine sm(TestConnState::DISCONNECTED); - - sm.addTransition(TestConnState::DISCONNECTED, TestConnEvent::CONNECT, - TestConnState::CONNECTING); - - std::mutex mutex; - std::condition_variable cv; - bool done = false; - Result async_result = - Result(Error(-1, "Not completed")); - - sm.triggerAsync(TestConnEvent::CONNECT, *dispatcher_, - [&](Result result) { - std::lock_guard lock(mutex); - async_result = std::move(result); - done = true; - cv.notify_one(); - }); - - // Run dispatcher until done - while (true) { - { - std::unique_lock lock(mutex); - if (done) - break; - } - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - EXPECT_TRUE(mcp::holds_alternative(async_result)); - EXPECT_EQ(mcp::get(async_result), TestConnState::CONNECTING); - EXPECT_EQ(sm.currentState(), TestConnState::CONNECTING); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/timeout_test.cc b/third_party/gopher-orch/tests/gopher/orch/timeout_test.cc deleted file mode 100644 index 382c67be..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/timeout_test.cc +++ /dev/null @@ -1,64 +0,0 @@ -// Unit tests for Timeout resilience pattern - -#include "orch_test_fixture.h" - -// ============================================================================= -// Timeout Tests -// ============================================================================= - -TEST_F(OrchTest, TimeoutSuccess) { - // Operation completes before timeout - auto fastLambda = makeJsonLambda( - [](const JsonValue&) -> Result { - JsonValue result = JsonValue::object(); - result["completed"] = JsonValue(true); - return makeSuccess(JsonValue(result)); - }, - "FastLambda"); - - auto timeoutLambda = withTimeout(fastLambda, 1000); // 1 second timeout - - JsonValue result = - runToCompletion([&](Dispatcher& d, JsonCallback cb) { - timeoutLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_TRUE(result["completed"].getBool()); -} - -TEST_F(OrchTest, TimeoutExpired) { - // Operation takes longer than timeout - // Use shared_ptr to keep timer alive until it fires - struct TimerHolder { - mcp::event::TimerPtr timer; - }; - - auto slowLambda = makeLambdaAsync( - [](const JsonValue&, const RunnableConfig&, Dispatcher& dispatcher, - JsonCallback callback) { - // Create holder to keep timer alive - auto holder = std::make_shared(); - - // Schedule completion after 500ms - but timeout is 50ms - holder->timer = dispatcher.createTimer( - [callback = std::move(callback), holder]() mutable { - JsonValue result = JsonValue::object(); - result["completed"] = JsonValue(true); - callback(makeSuccess(JsonValue(result))); - }); - holder->timer->enableTimer(std::chrono::milliseconds(500)); - }, - "SlowLambda"); - - auto timeoutLambda = withTimeout(slowLambda, 50); // 50ms timeout - - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - timeoutLambda->invoke(JsonValue::object(), RunnableConfig(), d, - std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).code, OrchError::TIMEOUT); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/tool_registry_test.cc b/third_party/gopher-orch/tests/gopher/orch/tool_registry_test.cc deleted file mode 100644 index a6b29da2..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/tool_registry_test.cc +++ /dev/null @@ -1,766 +0,0 @@ -// Unit tests for ToolRegistry and ToolExecutor - -#include "gopher/orch/agent/tool_registry.h" - -#include "gopher/orch/agent/config_loader.h" -#include "gopher/orch/agent/tool_definition.h" -#include "gopher/orch/agent/tool_executor.h" -#include "gopher/orch/server/mock_server.h" -#include "orch_test_fixture.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; -using namespace gopher::orch::server; - -// ============================================================================= -// ToolRegistry Test Fixture -// ============================================================================= - -class ToolRegistryTest : public OrchTest { - protected: - ToolRegistryPtr registry_; - ToolExecutorPtr executor_; - std::shared_ptr mock_server_; - - void SetUp() override { - OrchTest::SetUp(); - registry_ = makeToolRegistry(); - executor_ = makeToolExecutor(registry_); - mock_server_ = makeMockServer("test-server"); - } - - // Helper to build a simple JSON schema - JsonValue makeSchema(const std::string& type = "object") { - JsonValue schema = JsonValue::object(); - schema["type"] = type; - return schema; - } - - // Helper to build a schema with properties - JsonValue makeSchemaWithProps( - const std::map& props) { - JsonValue schema = JsonValue::object(); - schema["type"] = "object"; - - JsonValue properties = JsonValue::object(); - for (const auto& kv : props) { - JsonValue prop = JsonValue::object(); - prop["type"] = kv.second; - properties[kv.first] = prop; - } - schema["properties"] = properties; - - return schema; - } -}; - -// ============================================================================= -// Basic Tool Registration Tests -// ============================================================================= - -TEST_F(ToolRegistryTest, CreateEmpty) { - EXPECT_EQ(registry_->toolCount(), 0u); - EXPECT_TRUE(registry_->getToolSpecs().empty()); - EXPECT_TRUE(registry_->getToolNames().empty()); -} - -TEST_F(ToolRegistryTest, AddLocalTool) { - registry_->addTool("calculator", "Perform calculations", makeSchema(), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - cb(Result(JsonValue(42))); - }); - - EXPECT_EQ(registry_->toolCount(), 1u); - EXPECT_TRUE(registry_->hasTool("calculator")); - EXPECT_FALSE(registry_->hasTool("nonexistent")); - - auto specs = registry_->getToolSpecs(); - ASSERT_EQ(specs.size(), 1u); - EXPECT_EQ(specs[0].name, "calculator"); - EXPECT_EQ(specs[0].description, "Perform calculations"); -} - -TEST_F(ToolRegistryTest, AddToolWithSpec) { - ToolSpec spec("search", "Search the web", makeSchema()); - registry_->addTool(spec, - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - cb(Result(JsonValue("search result"))); - }); - - EXPECT_TRUE(registry_->hasTool("search")); - - auto retrieved = registry_->getToolSpec("search"); - ASSERT_TRUE(retrieved.has_value()); - EXPECT_EQ(retrieved->name, "search"); - EXPECT_EQ(retrieved->description, "Search the web"); -} - -TEST_F(ToolRegistryTest, AddSyncTool) { - registry_->addSyncTool("sync_calc", "Synchronous calculation", makeSchema(), - [](const JsonValue& args) -> Result { - return Result(JsonValue(100)); - }); - - EXPECT_TRUE(registry_->hasTool("sync_calc")); - - auto result = runToCompletion([&](Dispatcher& d, JsonCallback cb) { - executor_->executeTool("sync_calc", JsonValue::object(), d, std::move(cb)); - }); - - EXPECT_EQ(result.getInt(), 100); -} - -TEST_F(ToolRegistryTest, AddMultipleTools) { - registry_->addTool("tool1", "Tool 1", makeSchema(), - [](const JsonValue&, Dispatcher&, JsonCallback cb) { - cb(Result(JsonValue(1))); - }); - - registry_->addTool("tool2", "Tool 2", makeSchema(), - [](const JsonValue&, Dispatcher&, JsonCallback cb) { - cb(Result(JsonValue(2))); - }); - - registry_->addTool("tool3", "Tool 3", makeSchema(), - [](const JsonValue&, Dispatcher&, JsonCallback cb) { - cb(Result(JsonValue(3))); - }); - - EXPECT_EQ(registry_->toolCount(), 3u); - - auto names = registry_->getToolNames(); - EXPECT_EQ(names.size(), 3u); - EXPECT_TRUE(std::find(names.begin(), names.end(), "tool1") != names.end()); - EXPECT_TRUE(std::find(names.begin(), names.end(), "tool2") != names.end()); - EXPECT_TRUE(std::find(names.begin(), names.end(), "tool3") != names.end()); -} - -// ============================================================================= -// Tool Execution Tests (via ToolExecutor) -// ============================================================================= - -TEST_F(ToolRegistryTest, ExecuteLocalTool) { - registry_->addTool("echo", "Echo input", makeSchema(), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - JsonValue result = JsonValue::object(); - result["echoed"] = args; - cb(Result(std::move(result))); - }); - - JsonValue input = JsonValue::object(); - input["message"] = "hello"; - - auto result = runToCompletion([&](Dispatcher& d, JsonCallback cb) { - executor_->executeTool("echo", input, d, std::move(cb)); - }); - - EXPECT_TRUE(result.contains("echoed")); - EXPECT_EQ(result["echoed"]["message"].getString(), "hello"); -} - -TEST_F(ToolRegistryTest, ExecuteToolNotFound) { - auto result = - runToCompletionResult([&](Dispatcher& d, JsonCallback cb) { - executor_->executeTool("nonexistent", JsonValue::object(), d, - std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - auto error = mcp::get(result); - EXPECT_TRUE(error.message.find("not found") != std::string::npos); -} - -TEST_F(ToolRegistryTest, ExecuteToolWithError) { - registry_->addSyncTool( - "failing", "Always fails", makeSchema(), - [](const JsonValue&) -> Result { - return Result(Error(-1, "Intentional failure")); - }); - - auto result = runToCompletionResult([&](Dispatcher& d, - JsonCallback cb) { - executor_->executeTool("failing", JsonValue::object(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "Intentional failure"); -} - -TEST_F(ToolRegistryTest, ExecuteToolCall) { - registry_->addSyncTool("greet", "Greet someone", makeSchema(), - [](const JsonValue& args) -> Result { - std::string name = args.contains("name") - ? args["name"].getString() - : "World"; - JsonValue result = JsonValue::object(); - result["greeting"] = "Hello, " + name + "!"; - return Result(result); - }); - - JsonValue args = JsonValue::object(); - args["name"] = "Alice"; - - ToolCall call("call_123", "greet", args); - - auto result = runToCompletion([&](Dispatcher& d, JsonCallback cb) { - executor_->executeToolCall(call, d, std::move(cb)); - }); - - EXPECT_EQ(result["greeting"].getString(), "Hello, Alice!"); -} - -TEST_F(ToolRegistryTest, ExecuteMultipleToolCalls) { - registry_->addSyncTool("double", "Double a number", makeSchema(), - [](const JsonValue& args) -> Result { - int n = args.contains("n") ? args["n"].getInt() : 0; - return Result(JsonValue(n * 2)); - }); - - registry_->addSyncTool("triple", "Triple a number", makeSchema(), - [](const JsonValue& args) -> Result { - int n = args.contains("n") ? args["n"].getInt() : 0; - return Result(JsonValue(n * 3)); - }); - - JsonValue args1 = JsonValue::object(); - args1["n"] = 5; - JsonValue args2 = JsonValue::object(); - args2["n"] = 10; - - std::vector calls = {ToolCall("call_1", "double", args1), - ToolCall("call_2", "triple", args2)}; - - std::vector> results; - - std::mutex mutex; - std::condition_variable cv; - bool done = false; - - executor_->executeToolCalls(calls, true, *dispatcher_, - [&](std::vector> r) { - std::lock_guard lock(mutex); - results = std::move(r); - done = true; - cv.notify_one(); - }); - - while (true) { - { - std::unique_lock lock(mutex); - if (done) - break; - } - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - ASSERT_EQ(results.size(), 2u); - EXPECT_TRUE(mcp::holds_alternative(results[0])); - EXPECT_TRUE(mcp::holds_alternative(results[1])); - EXPECT_EQ(mcp::get(results[0]).getInt(), 10); // 5 * 2 - EXPECT_EQ(mcp::get(results[1]).getInt(), 30); // 10 * 3 -} - -// ============================================================================= -// Server Integration Tests -// ============================================================================= - -TEST_F(ToolRegistryTest, AddServerWithToolList) { - // Add tools to mock server - mock_server_->addTool("server_tool1", "Server tool 1"); - mock_server_->addTool("server_tool2", "Server tool 2"); - mock_server_->setResponse("server_tool1", JsonValue("result1")); - mock_server_->setResponse("server_tool2", JsonValue("result2")); - - // Connect server - mock_server_->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - // Get tool list from server - auto tools = runToCompletion>( - [&](Dispatcher& d, ServerToolListCallback cb) { - mock_server_->listTools(d, std::move(cb)); - }); - - // Add server with tools - registry_->addServer(mock_server_, tools); - - EXPECT_TRUE(registry_->hasTool("server_tool1")); - EXPECT_TRUE(registry_->hasTool("server_tool2")); - - // Check prefixed names also work - EXPECT_TRUE(registry_->hasTool("test-server:server_tool1")); -} - -TEST_F(ToolRegistryTest, ExecuteServerTool) { - mock_server_->addTool("remote_calc", "Remote calculation"); - - JsonValue calc_result = JsonValue::object(); - calc_result["answer"] = 42; - mock_server_->setResponse("remote_calc", calc_result); - - mock_server_->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - auto tools = runToCompletion>( - [&](Dispatcher& d, ServerToolListCallback cb) { - mock_server_->listTools(d, std::move(cb)); - }); - - registry_->addServer(mock_server_, tools); - - auto result = runToCompletion([&](Dispatcher& d, JsonCallback cb) { - executor_->executeTool("remote_calc", JsonValue::object(), d, - std::move(cb)); - }); - - EXPECT_EQ(result["answer"].getInt(), 42); - EXPECT_EQ(mock_server_->callCount("remote_calc"), 1u); -} - -TEST_F(ToolRegistryTest, AddServerToolWithAlias) { - mock_server_->addTool("original_name", "Original tool"); - mock_server_->setResponse("original_name", JsonValue("ok")); - - mock_server_->connect(*dispatcher_, [](Result) {}); - dispatcher_->run(mcp::event::RunType::NonBlock); - - ServerToolInfo info("original_name", "Original tool"); - registry_->addServerTool(mock_server_, info, "aliased_name"); - - EXPECT_TRUE(registry_->hasTool("aliased_name")); - EXPECT_FALSE(registry_->hasTool("original_name")); - - // Execute via alias - auto result = runToCompletion([&](Dispatcher& d, JsonCallback cb) { - executor_->executeTool("aliased_name", JsonValue::object(), d, - std::move(cb)); - }); - - EXPECT_EQ(result.getString(), "ok"); -} - -// ============================================================================= -// Tool Management Tests -// ============================================================================= - -TEST_F(ToolRegistryTest, RemoveTool) { - registry_->addSyncTool("temp_tool", "Temporary", makeSchema(), - [](const JsonValue&) -> Result { - return Result(JsonValue("temp")); - }); - - EXPECT_TRUE(registry_->hasTool("temp_tool")); - EXPECT_EQ(registry_->toolCount(), 1u); - - registry_->removeTool("temp_tool"); - - EXPECT_FALSE(registry_->hasTool("temp_tool")); - EXPECT_EQ(registry_->toolCount(), 0u); -} - -TEST_F(ToolRegistryTest, Clear) { - registry_->addTool("tool1", "Tool 1", makeSchema(), - [](const JsonValue&, Dispatcher&, JsonCallback cb) { - cb(Result(JsonValue(1))); - }); - registry_->addTool("tool2", "Tool 2", makeSchema(), - [](const JsonValue&, Dispatcher&, JsonCallback cb) { - cb(Result(JsonValue(2))); - }); - - EXPECT_EQ(registry_->toolCount(), 2u); - - registry_->clear(); - - EXPECT_EQ(registry_->toolCount(), 0u); - EXPECT_TRUE(registry_->getToolSpecs().empty()); -} - -TEST_F(ToolRegistryTest, GetToolEntry) { - registry_->addTool("local_tool", "Local", makeSchema(), - [](const JsonValue&, Dispatcher&, JsonCallback cb) { - cb(Result(JsonValue("local"))); - }); - - auto entry = registry_->getToolEntry("local_tool"); - ASSERT_TRUE(entry.has_value()); - EXPECT_EQ(entry->spec.name, "local_tool"); - EXPECT_TRUE(entry->isLocal()); - EXPECT_FALSE(entry->isRemote()); - EXPECT_EQ(entry->server, nullptr); - - auto missing = registry_->getToolEntry("nonexistent"); - EXPECT_FALSE(missing.has_value()); -} - -// ============================================================================= -// Conversion Utility Tests -// ============================================================================= - -TEST(ToolConversionTest, ServerToolInfoToToolSpec) { - ServerToolInfo info; - info.name = "test_tool"; - info.description = "Test description"; - info.inputSchema = JsonValue::object(); - info.inputSchema["type"] = "object"; - - ToolSpec spec = toToolSpec(info); - - EXPECT_EQ(spec.name, "test_tool"); - EXPECT_EQ(spec.description, "Test description"); - EXPECT_TRUE(spec.parameters.contains("type")); -} - -TEST(ToolConversionTest, ToolSpecToServerToolInfo) { - ToolSpec spec; - spec.name = "another_tool"; - spec.description = "Another description"; - spec.parameters = JsonValue::object(); - spec.parameters["type"] = "object"; - - ServerToolInfo info = toServerToolInfo(spec); - - EXPECT_EQ(info.name, "another_tool"); - EXPECT_EQ(info.description, "Another description"); - EXPECT_TRUE(info.inputSchema.contains("type")); -} - -// ============================================================================= -// Environment Variable Tests -// ============================================================================= - -TEST_F(ToolRegistryTest, SetEnvVariable) { - registry_->setEnv("API_KEY", "secret123"); - registry_->setEnv("BASE_URL", "https://api.example.com"); - - // Env vars are used during config loading - // This test just verifies they can be set without errors - SUCCEED(); -} - -// ============================================================================= -// ConfigLoader Tests -// ============================================================================= - -class ConfigLoaderTest : public OrchTest { - protected: - ConfigLoader loader_; - - void SetUp() override { - OrchTest::SetUp(); - loader_.setEnv("API_KEY", "test-key-123"); - loader_.setEnv("BASE_URL", "https://api.test.com"); - } -}; - -TEST_F(ConfigLoaderTest, SubstituteEnvVars) { - std::string input = "Key: ${API_KEY}, URL: ${BASE_URL}"; - std::string result = loader_.substituteEnvVars(input); - - EXPECT_EQ(result, "Key: test-key-123, URL: https://api.test.com"); -} - -TEST_F(ConfigLoaderTest, SubstituteUnknownVar) { - std::string input = "Unknown: ${UNKNOWN_VAR}"; - std::string result = loader_.substituteEnvVars(input); - - // Unknown variables are replaced with empty string - EXPECT_EQ(result, "Unknown: "); -} - -TEST_F(ConfigLoaderTest, ParseHttpMethod) { - // Access private method via public API - // We test this indirectly through tool definition parsing - std::string json = R"({ - "name": "test_tool", - "description": "Test", - "rest_endpoint": { - "method": "POST", - "url": "https://api.test.com/endpoint" - } - })"; - - auto result = loader_.loadFromString("{\"tools\": [" + json + "]}"); - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto config = mcp::get(result); - ASSERT_EQ(config.tools.size(), 1u); - ASSERT_TRUE(config.tools[0].rest_endpoint.has_value()); - EXPECT_EQ(config.tools[0].rest_endpoint->method, HttpMethod::POST); -} - -TEST_F(ConfigLoaderTest, ParseToolDefinition) { - std::string json = R"({ - "name": "search", - "description": "Search the web", - "input_schema": { - "type": "object", - "properties": { - "query": {"type": "string"} - } - }, - "tags": ["search", "web"], - "require_approval": true - })"; - - auto result = loader_.parseToolDefinition(JsonValue::parse(json)); - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto def = mcp::get(result); - EXPECT_EQ(def.name, "search"); - EXPECT_EQ(def.description, "Search the web"); - EXPECT_EQ(def.tags.size(), 2u); - EXPECT_TRUE(def.require_approval); - EXPECT_TRUE(def.input_schema.contains("properties")); -} - -TEST_F(ConfigLoaderTest, ParseToolDefinitionMissingName) { - std::string json = R"({ - "description": "No name provided" - })"; - - auto result = loader_.parseToolDefinition(JsonValue::parse(json)); - EXPECT_TRUE(mcp::holds_alternative(result)); -} - -TEST_F(ConfigLoaderTest, ParseMCPServerDefinition) { - std::string json = R"({ - "name": "mcp-server", - "transport": "stdio", - "stdio": { - "command": "node", - "args": ["server.js"], - "working_directory": "/app" - }, - "connect_timeout_ms": 5000, - "request_timeout_ms": 30000, - "max_retries": 3 - })"; - - auto result = loader_.parseMCPServerDefinition(JsonValue::parse(json)); - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto def = mcp::get(result); - EXPECT_EQ(def.name, "mcp-server"); - EXPECT_EQ(def.transport, MCPServerDefinition::TransportType::STDIO); - ASSERT_TRUE(def.stdio_config.has_value()); - EXPECT_EQ(def.stdio_config->command, "node"); - EXPECT_EQ(def.stdio_config->args.size(), 1u); - EXPECT_EQ(def.stdio_config->args[0], "server.js"); - EXPECT_EQ(def.connect_timeout, std::chrono::milliseconds(5000)); - EXPECT_EQ(def.request_timeout, std::chrono::milliseconds(30000)); - EXPECT_EQ(def.max_retries, 3u); -} - -TEST_F(ConfigLoaderTest, ParseHTTPSSEServer) { - std::string json = R"({ - "name": "sse-server", - "transport": "http_sse", - "http_sse": { - "url": "${BASE_URL}/sse", - "headers": { - "Authorization": "Bearer ${API_KEY}" - }, - "verify_ssl": false - } - })"; - - auto result = loader_.parseMCPServerDefinition(JsonValue::parse(json)); - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto def = mcp::get(result); - EXPECT_EQ(def.transport, MCPServerDefinition::TransportType::HTTP_SSE); - ASSERT_TRUE(def.http_sse_config.has_value()); - EXPECT_EQ(def.http_sse_config->url, "https://api.test.com/sse"); - EXPECT_EQ(def.http_sse_config->headers["Authorization"], - "Bearer test-key-123"); - EXPECT_FALSE(def.http_sse_config->verify_ssl); -} - -TEST_F(ConfigLoaderTest, ParseAuthPreset) { - std::string json = R"({ - "type": "bearer", - "value": "${API_KEY}", - "header": "X-Custom-Auth" - })"; - - auto result = loader_.parseAuthPreset(JsonValue::parse(json)); - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto auth = mcp::get(result); - EXPECT_EQ(auth.type, AuthPreset::Type::BEARER); - EXPECT_EQ(auth.value, "test-key-123"); - EXPECT_EQ(auth.header, "X-Custom-Auth"); -} - -TEST_F(ConfigLoaderTest, LoadFromString) { - std::string json = R"({ - "name": "test-registry", - "base_url": "${BASE_URL}", - "default_headers": { - "X-API-Key": "${API_KEY}" - }, - "tools": [ - { - "name": "tool1", - "description": "First tool" - }, - { - "name": "tool2", - "description": "Second tool" - } - ], - "mcp_servers": [ - { - "name": "server1", - "transport": "stdio", - "stdio": { - "command": "node", - "args": ["server.js"] - } - } - ] - })"; - - auto result = loader_.loadFromString(json); - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto config = mcp::get(result); - EXPECT_EQ(config.name, "test-registry"); - EXPECT_EQ(config.base_url, "https://api.test.com"); - EXPECT_EQ(config.default_headers["X-API-Key"], "test-key-123"); - EXPECT_EQ(config.tools.size(), 2u); - EXPECT_EQ(config.mcp_servers.size(), 1u); -} - -TEST_F(ConfigLoaderTest, LoadFromStringInvalidJson) { - std::string invalid_json = "{ invalid json }"; - - auto result = loader_.loadFromString(invalid_json); - EXPECT_TRUE(mcp::holds_alternative(result)); -} - -// ============================================================================= -// ToolDefinition Tests -// ============================================================================= - -TEST(ToolDefinitionTest, ToToolSpec) { - ToolDefinition def; - def.name = "test_tool"; - def.description = "Test description"; - def.input_schema = JsonValue::object(); - def.input_schema["type"] = "object"; - - ToolSpec spec = def.toToolSpec(); - - EXPECT_EQ(spec.name, "test_tool"); - EXPECT_EQ(spec.description, "Test description"); - EXPECT_TRUE(spec.parameters.contains("type")); -} - -TEST(ToolDefinitionTest, RESTEndpoint) { - ToolDefinition::RESTEndpoint rest; - rest.method = HttpMethod::POST; - rest.url = "https://api.example.com/search"; - rest.headers["Content-Type"] = "application/json"; - rest.body_mapping["query"] = "$.input.query"; - - EXPECT_EQ(rest.method, HttpMethod::POST); - EXPECT_EQ(rest.url, "https://api.example.com/search"); - EXPECT_EQ(rest.headers["Content-Type"], "application/json"); -} - -TEST(ToolDefinitionTest, MCPToolRef) { - ToolDefinition::MCPToolRef ref; - ref.server_name = "mcp-server"; - ref.tool_name = "remote_tool"; - - EXPECT_EQ(ref.server_name, "mcp-server"); - EXPECT_EQ(ref.tool_name, "remote_tool"); -} - -TEST(MCPServerDefinitionTest, TransportTypes) { - MCPServerDefinition stdio_server; - stdio_server.transport = MCPServerDefinition::TransportType::STDIO; - EXPECT_EQ(stdio_server.transport, MCPServerDefinition::TransportType::STDIO); - - MCPServerDefinition sse_server; - sse_server.transport = MCPServerDefinition::TransportType::HTTP_SSE; - EXPECT_EQ(sse_server.transport, MCPServerDefinition::TransportType::HTTP_SSE); - - MCPServerDefinition ws_server; - ws_server.transport = MCPServerDefinition::TransportType::WEBSOCKET; - EXPECT_EQ(ws_server.transport, MCPServerDefinition::TransportType::WEBSOCKET); -} - -TEST(AuthPresetTest, Types) { - AuthPreset bearer; - bearer.type = AuthPreset::Type::BEARER; - bearer.value = "token123"; - EXPECT_EQ(bearer.type, AuthPreset::Type::BEARER); - - AuthPreset api_key; - api_key.type = AuthPreset::Type::API_KEY; - api_key.value = "key123"; - api_key.header = "X-API-Key"; - EXPECT_EQ(api_key.type, AuthPreset::Type::API_KEY); - - AuthPreset basic; - basic.type = AuthPreset::Type::BASIC; - basic.value = "user:pass"; - EXPECT_EQ(basic.type, AuthPreset::Type::BASIC); -} - -// ============================================================================= -// ToolExecutor Tests -// ============================================================================= - -class ToolExecutorTest : public OrchTest { - protected: - ToolRegistryPtr registry_; - ToolExecutorPtr executor_; - - void SetUp() override { - OrchTest::SetUp(); - registry_ = makeToolRegistry(); - executor_ = makeToolExecutor(registry_); - } -}; - -TEST_F(ToolExecutorTest, CreateExecutor) { - EXPECT_NE(executor_, nullptr); - EXPECT_EQ(executor_->registry(), registry_); -} - -TEST_F(ToolExecutorTest, ExecuteWithNoRegistry) { - auto executor = makeToolExecutor(nullptr); - - auto result = runToCompletionResult([&](Dispatcher& d, - JsonCallback cb) { - executor->executeTool("any_tool", JsonValue::object(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - auto error = mcp::get(result); - EXPECT_TRUE(error.message.find("No registry") != std::string::npos); -} - -TEST_F(ToolExecutorTest, ExecuteEmptyToolCalls) { - std::vector empty_calls; - std::vector> results; - bool done = false; - - executor_->executeToolCalls(empty_calls, true, *dispatcher_, - [&](std::vector> r) { - results = std::move(r); - done = true; - }); - - while (!done) { - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - EXPECT_TRUE(results.empty()); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/tool_runnable_test.cc b/third_party/gopher-orch/tests/gopher/orch/tool_runnable_test.cc deleted file mode 100644 index 3df740cc..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/tool_runnable_test.cc +++ /dev/null @@ -1,389 +0,0 @@ -// Unit tests for ToolRunnable - -#include "gopher/orch/agent/tool_runnable.h" - -#include "orch_test_fixture.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::llm; -using namespace gopher::orch::core; - -// ============================================================================= -// ToolRunnable Test Fixture -// ============================================================================= - -class ToolRunnableTest : public OrchTest { - protected: - ToolRegistryPtr registry_; - ToolExecutorPtr executor_; - ToolRunnable::Ptr tool_runnable_; - - void SetUp() override { - OrchTest::SetUp(); - registry_ = makeToolRegistry(); - executor_ = makeToolExecutor(registry_); - tool_runnable_ = ToolRunnable::create(executor_); - - // Add some test tools - addTestTools(); - } - - void addTestTools() { - // Calculator tool - synchronous - registry_->addSyncTool( - "calculator", "Perform calculations", makeSchema(), - [](const JsonValue& args) -> Result { - if (args.contains("expression") && args["expression"].isString()) { - std::string expr = args["expression"].getString(); - if (expr == "2+2") { - return Result(JsonValue(4)); - } - } - return Result(JsonValue(0)); - }); - - // Search tool - asynchronous - registry_->addTool( - "search", "Search the web", makeSchema(), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - std::string query = "default"; - if (args.contains("query") && args["query"].isString()) { - query = args["query"].getString(); - } - - JsonValue result = JsonValue::object(); - result["query"] = query; - result["results"] = JsonValue::array(); - - d.post([cb = std::move(cb), result = std::move(result)]() mutable { - cb(Result(std::move(result))); - }); - }); - - // Failing tool - registry_->addTool( - "failing_tool", "Always fails", makeSchema(), - [](const JsonValue& args, Dispatcher& d, JsonCallback cb) { - d.post([cb = std::move(cb)]() { - cb(Result(Error(-1, "Tool execution failed"))); - }); - }); - } - - JsonValue makeSchema() { - JsonValue schema = JsonValue::object(); - schema["type"] = "object"; - return schema; - } -}; - -// ============================================================================= -// Basic Tests -// ============================================================================= - -TEST_F(ToolRunnableTest, Name) { - EXPECT_EQ(tool_runnable_->name(), "ToolRunnable"); -} - -TEST_F(ToolRunnableTest, Accessors) { - EXPECT_EQ(tool_runnable_->executor(), executor_); - EXPECT_EQ(tool_runnable_->registry(), registry_); -} - -// ============================================================================= -// Single Tool Call Tests -// ============================================================================= - -TEST_F(ToolRunnableTest, SingleToolCall) { - JsonValue input = JsonValue::object(); - input["name"] = "calculator"; - JsonValue args = JsonValue::object(); - args["expression"] = "2+2"; - input["arguments"] = args; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result.isObject()); - EXPECT_TRUE(result["success"].getBool()); - EXPECT_EQ(result["result"].getInt(), 4); -} - -TEST_F(ToolRunnableTest, SingleToolCallWithId) { - JsonValue input = JsonValue::object(); - input["id"] = "call_123"; - input["name"] = "calculator"; - JsonValue args = JsonValue::object(); - args["expression"] = "2+2"; - input["arguments"] = args; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result["success"].getBool()); - EXPECT_EQ(result["id"].getString(), "call_123"); - EXPECT_EQ(result["result"].getInt(), 4); -} - -TEST_F(ToolRunnableTest, AsyncToolCall) { - JsonValue input = JsonValue::object(); - input["name"] = "search"; - JsonValue args = JsonValue::object(); - args["query"] = "weather in tokyo"; - input["arguments"] = args; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result["success"].getBool()); - EXPECT_TRUE(result["result"].isObject()); - EXPECT_EQ(result["result"]["query"].getString(), "weather in tokyo"); -} - -TEST_F(ToolRunnableTest, ToolNotFound) { - JsonValue input = JsonValue::object(); - input["name"] = "nonexistent_tool"; - input["arguments"] = JsonValue::object(); - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // Should return success with error in JSON, not fail the Result - EXPECT_TRUE(result.isObject()); - EXPECT_FALSE(result["success"].getBool()); - EXPECT_TRUE(result.contains("error")); -} - -TEST_F(ToolRunnableTest, ToolExecutionFails) { - JsonValue input = JsonValue::object(); - input["id"] = "call_fail"; - input["name"] = "failing_tool"; - input["arguments"] = JsonValue::object(); - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_FALSE(result["success"].getBool()); - EXPECT_EQ(result["id"].getString(), "call_fail"); - EXPECT_EQ(result["error"].getString(), "Tool execution failed"); -} - -TEST_F(ToolRunnableTest, MissingToolName) { - JsonValue input = JsonValue::object(); - input["arguments"] = JsonValue::object(); - // No "name" field - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, - "Invalid tool call input: missing 'name' field"); -} - -TEST_F(ToolRunnableTest, DefaultArguments) { - // Arguments should default to empty object if not provided - JsonValue input = JsonValue::object(); - input["name"] = "search"; - // No "arguments" field - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result["success"].getBool()); - EXPECT_EQ(result["result"]["query"].getString(), "default"); -} - -// ============================================================================= -// Multiple Tool Calls Tests -// ============================================================================= - -TEST_F(ToolRunnableTest, MultipleToolCalls) { - JsonValue input = JsonValue::object(); - JsonValue calls = JsonValue::array(); - - // First call - JsonValue call1 = JsonValue::object(); - call1["id"] = "call_1"; - call1["name"] = "calculator"; - JsonValue args1 = JsonValue::object(); - args1["expression"] = "2+2"; - call1["arguments"] = args1; - calls.push_back(call1); - - // Second call - JsonValue call2 = JsonValue::object(); - call2["id"] = "call_2"; - call2["name"] = "search"; - JsonValue args2 = JsonValue::object(); - args2["query"] = "test query"; - call2["arguments"] = args2; - calls.push_back(call2); - - input["tool_calls"] = calls; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(result.contains("results")); - EXPECT_TRUE(result["results"].isArray()); - EXPECT_EQ(result["results"].size(), 2u); - - // First result - auto& result1 = result["results"][0]; - EXPECT_EQ(result1["id"].getString(), "call_1"); - EXPECT_TRUE(result1["success"].getBool()); - EXPECT_EQ(result1["result"].getInt(), 4); - - // Second result - auto& result2 = result["results"][1]; - EXPECT_EQ(result2["id"].getString(), "call_2"); - EXPECT_TRUE(result2["success"].getBool()); - EXPECT_EQ(result2["result"]["query"].getString(), "test query"); -} - -TEST_F(ToolRunnableTest, MultipleToolCallsWithFailure) { - JsonValue input = JsonValue::object(); - JsonValue calls = JsonValue::array(); - - // Successful call - JsonValue call1 = JsonValue::object(); - call1["id"] = "call_1"; - call1["name"] = "calculator"; - JsonValue args1 = JsonValue::object(); - args1["expression"] = "2+2"; - call1["arguments"] = args1; - calls.push_back(call1); - - // Failing call - JsonValue call2 = JsonValue::object(); - call2["id"] = "call_2"; - call2["name"] = "failing_tool"; - call2["arguments"] = JsonValue::object(); - calls.push_back(call2); - - input["tool_calls"] = calls; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_EQ(result["results"].size(), 2u); - - // First should succeed - EXPECT_TRUE(result["results"][0]["success"].getBool()); - - // Second should fail - EXPECT_FALSE(result["results"][1]["success"].getBool()); - EXPECT_EQ(result["results"][1]["error"].getString(), "Tool execution failed"); -} - -TEST_F(ToolRunnableTest, MultipleToolCallsAutoGenerateIds) { - JsonValue input = JsonValue::object(); - JsonValue calls = JsonValue::array(); - - // Call without id - JsonValue call1 = JsonValue::object(); - call1["name"] = "calculator"; - JsonValue args1 = JsonValue::object(); - args1["expression"] = "2+2"; - call1["arguments"] = args1; - calls.push_back(call1); - - // Another call without id - JsonValue call2 = JsonValue::object(); - call2["name"] = "search"; - JsonValue args2 = JsonValue::object(); - args2["query"] = "test"; - call2["arguments"] = args2; - calls.push_back(call2); - - input["tool_calls"] = calls; - - auto result = runToCompletion( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - // IDs should be auto-generated as "call_0", "call_1" - EXPECT_EQ(result["results"][0]["id"].getString(), "call_0"); - EXPECT_EQ(result["results"][1]["id"].getString(), "call_1"); -} - -TEST_F(ToolRunnableTest, EmptyToolCallsArray) { - JsonValue input = JsonValue::object(); - input["tool_calls"] = JsonValue::array(); - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "Empty tool_calls array"); -} - -// ============================================================================= -// Error Cases -// ============================================================================= - -TEST_F(ToolRunnableTest, NoExecutorError) { - auto runnable_no_executor = ToolRunnable::create(nullptr); - - JsonValue input = JsonValue::object(); - input["name"] = "test"; - input["arguments"] = JsonValue::object(); - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - runnable_no_executor->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "No tool executor configured"); -} - -TEST_F(ToolRunnableTest, InvalidInputType) { - // Non-object input - JsonValue input = JsonValue::array(); - - auto result = runToCompletionResult( - [&](Dispatcher& d, ResultCallback cb) { - tool_runnable_->invoke(input, RunnableConfig(), d, std::move(cb)); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); -} - -// ============================================================================= -// Factory Function Tests -// ============================================================================= - -TEST_F(ToolRunnableTest, MakeToolRunnableFromExecutor) { - auto runnable = makeToolRunnable(executor_); - EXPECT_NE(runnable, nullptr); - EXPECT_EQ(runnable->executor(), executor_); -} - -TEST_F(ToolRunnableTest, MakeToolRunnableFromRegistry) { - auto runnable = makeToolRunnable(registry_); - EXPECT_NE(runnable, nullptr); - EXPECT_EQ(runnable->registry(), registry_); -} diff --git a/third_party/gopher-orch/tests/gopher/orch/tools_fetcher_integration_test.cpp b/third_party/gopher-orch/tests/gopher/orch/tools_fetcher_integration_test.cpp deleted file mode 100644 index 7fc34cfc..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/tools_fetcher_integration_test.cpp +++ /dev/null @@ -1,501 +0,0 @@ -// Integration tests for ToolsFetcher end-to-end functionality - -#include "gopher/orch/agent/tools_fetcher.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/agent/config_loader.h" -#include "gopher/orch/agent/tool_executor.h" -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/server/server_composite.h" -#include "orch_test_fixture.h" -#include "gtest/gtest.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::server; - -// ============================================================================= -// MockMCPServer - Simulates an HTTP+SSE MCP server for testing -// ============================================================================= - -class MockMCPServer { - public: - MockMCPServer(const std::string& name, int port) - : name_(name), port_(port), running_(false), should_fail_(false) { - // Define some mock tools - ServerToolInfo tool1; - tool1.name = name + "_tool1"; - tool1.description = "First tool for " + name; - tool1.inputSchema = JsonValue::object(); - tools_.push_back(tool1); - - ServerToolInfo tool2; - tool2.name = name + "_tool2"; - tool2.description = "Second tool for " + name; - tool2.inputSchema = JsonValue::object(); - tools_.push_back(tool2); - - ServerToolInfo tool3; - tool3.name = name + "_calculator"; - tool3.description = "Calculator for " + name; - tool3.inputSchema = JsonValue::object(); - tools_.push_back(tool3); - } - - ~MockMCPServer() { - stop(); - } - - // Start the mock server - void start() { - if (running_) return; - running_ = true; - - // In a real implementation, this would start an HTTP server - // For integration testing, we simulate a server without actually - // starting one since we can't easily bind to ports in unit tests - // The ToolsFetcher will fail to connect, which we handle gracefully - } - - // Stop the mock server - void stop() { - if (!running_) return; - running_ = false; - } - - // Simulate server failure - void simulateFailure() { - should_fail_ = true; - stop(); - } - - // Get list of tools this server provides - std::vector getTools() const { - return tools_; - } - - // Track if a tool was executed - bool wasToolExecuted(const std::string& tool_name) const { - return executed_tools_.count(tool_name) > 0; - } - - // Get execution count for a tool - int getExecutionCount(const std::string& tool_name) const { - auto it = executed_tools_.find(tool_name); - return it != executed_tools_.end() ? it->second : 0; - } - - bool isRunning() const { return running_; } - const std::string& getName() const { return name_; } - int getPort() const { return port_; } - - private: - JsonValue handleToolExecution(const std::string& tool_name, const JsonValue& args) { - if (should_fail_) { - throw std::runtime_error("Server is simulating failure"); - } - - // Track execution - executed_tools_[tool_name]++; - - // Return mock result - JsonValue result = JsonValue::object(); - result["tool"] = tool_name; - result["status"] = "success"; - result["result"] = "Mock execution of " + tool_name; - return result; - } - - std::string name_; - int port_; - std::atomic running_; - std::atomic should_fail_; - std::vector tools_; - mutable std::map executed_tools_; -}; - -// ============================================================================= -// Integration Test Fixture -// ============================================================================= - -class ToolsFetcherIntegrationTest : public OrchTest { - protected: - std::unique_ptr fetcher_; - std::vector> mock_servers_; - - void SetUp() override { - OrchTest::SetUp(); - fetcher_ = std::make_unique(); - } - - void TearDown() override { - // Stop all mock servers - for (auto& server : mock_servers_) { - server->stop(); - } - mock_servers_.clear(); - fetcher_.reset(); - OrchTest::TearDown(); - } - - // Create and start mock servers - void createMockServers(int count, int starting_port = 4000) { - for (int i = 0; i < count; ++i) { - auto server = std::make_unique( - "mock_server_" + std::to_string(i), - starting_port + i - ); - server->start(); - mock_servers_.push_back(std::move(server)); - } - } - - // Generate configuration JSON for mock servers - std::string generateConfig() { - std::string config = R"({"name": "integration-test-config", "mcp_servers": [)"; - - bool first = true; - for (const auto& mock_server : mock_servers_) { - if (!first) config += ","; - first = false; - - config += R"({ - "name": ")" + mock_server->getName() + R"(", - "transport": "http_sse", - "http_sse": { - "url": "http://localhost:)" + std::to_string(mock_server->getPort()) + R"(", - "headers": { - "Authorization": "Bearer test-key-)" + mock_server->getName() + R"(" - } - }, - "connect_timeout": 5000, - "request_timeout": 10000 - })"; - } - - config += "]}"; - return config; - } - - // Helper to wait for async operation - VoidResult waitForOperation( - std::function)> operation, - int timeout_seconds = 5) { - std::mutex mutex; - std::condition_variable cv; - bool done = false; - VoidResult result = VoidResult(Error(-1, "Operation not completed")); - - operation(*dispatcher_, [&](VoidResult r) { - std::lock_guard lock(mutex); - result = std::move(r); - done = true; - cv.notify_one(); - }); - - // Run dispatcher until done or timeout - auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_seconds); - while (!done && std::chrono::steady_clock::now() < timeout) { - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - return result; - } -}; - -// ============================================================================= -// Full Flow Tests -// ============================================================================= - -TEST_F(ToolsFetcherIntegrationTest, FullFlowWithMultipleServers) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - - // Start 3 mock servers - createMockServers(3); - - // Generate config pointing to mock servers - std::string config = generateConfig(); - - // Load config with ToolsFetcher - auto result = waitForOperation([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - // Note: In real integration tests, mock servers would actually be running - // Since we can't easily start real HTTP servers in unit tests, - // this will likely fail to connect but should handle it gracefully - - // Check that ToolsFetcher attempted to create the infrastructure - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); - - // In a real scenario with working servers: - // - Registry should have tools from all 3 servers - // - Composite should aggregate all servers - // - Tool execution should route to correct server -#else - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "MCP support not compiled in"); -#endif -} - -TEST_F(ToolsFetcherIntegrationTest, ToolDiscoveryAndExecution) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - -#ifdef GOPHER_ORCH_WITH_MCP - // Create 2 mock servers - createMockServers(2); - - // Load configuration - std::string config = generateConfig(); - auto load_result = waitForOperation([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - - auto registry = fetcher_->getRegistry(); - ASSERT_NE(registry, nullptr); - - // Create executor for tool execution - auto executor = makeToolExecutor(registry); - - // Try to execute a tool (would work with real servers) - // In this test environment, it will likely fail to connect - // but we're testing the integration setup - - JsonValue args = JsonValue::object(); - args["input"] = "test"; - - auto exec_result = waitForOperation([&executor, &args](Dispatcher& d, auto callback) { - executor->executeTool("mock_server_0_calculator", args, d, - [callback](Result r) { - if (mcp::holds_alternative(r)) { - callback(VoidResult(nullptr)); - } else { - callback(VoidResult(mcp::get(r))); - } - }); - }, 2); // Short timeout since servers aren't really running - - // Would verify execution reached mock server in real test -#else - GTEST_SKIP() << "MCP support not compiled in"; -#endif -} - -// ============================================================================= -// Error Scenario Tests -// ============================================================================= - -TEST_F(ToolsFetcherIntegrationTest, ServerUnavailable) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - - // Create config with non-existent server - std::string config = R"({ - "name": "test-config", - "mcp_servers": [ - { - "name": "unavailable-server", - "transport": "http_sse", - "http_sse": { - "url": "http://localhost:9999", - "headers": {"Authorization": "Bearer test"} - }, - "connect_timeout": 1000, - "request_timeout": 2000 - } - ] - })"; - - auto result = waitForOperation([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }, 3); // Short timeout for unavailable server - -#ifdef GOPHER_ORCH_WITH_MCP - // Should handle connection failure gracefully - // The exact behavior depends on error handling implementation - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -TEST_F(ToolsFetcherIntegrationTest, PartialServerFailure) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - - // Create 3 servers, simulate failure on one - createMockServers(3); - mock_servers_[1]->simulateFailure(); // Fail the middle server - - std::string config = generateConfig(); - auto result = waitForOperation([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - // Should still create registry with working servers - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); - - // In real test: verify that tools from servers 0 and 2 are available - // but not from server 1 -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -TEST_F(ToolsFetcherIntegrationTest, TimeoutHandling) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - - // Create config with very short timeout - std::string config = R"({ - "name": "timeout-test", - "mcp_servers": [ - { - "name": "slow-server", - "transport": "http_sse", - "http_sse": { - "url": "http://localhost:5555", - "headers": {} - }, - "connect_timeout": 100, - "request_timeout": 100 - } - ] - })"; - - auto start = std::chrono::steady_clock::now(); - auto result = waitForOperation([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }, 1); - auto duration = std::chrono::steady_clock::now() - start; - -#ifdef GOPHER_ORCH_WITH_MCP - // Should timeout quickly due to short timeout settings - EXPECT_LT(duration, std::chrono::seconds(2)); - - // Should still create infrastructure even if connection fails - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -// ============================================================================= -// Parallel Server Discovery Tests -// ============================================================================= - -TEST_F(ToolsFetcherIntegrationTest, ParallelServerDiscovery) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - - // Create 5 mock servers to test parallel connection - const int server_count = 5; - createMockServers(server_count); - - std::string config = generateConfig(); - - // Track timing to verify parallel execution - auto start = std::chrono::steady_clock::now(); - auto result = waitForOperation([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }, 10); - auto duration = std::chrono::steady_clock::now() - start; - -#ifdef GOPHER_ORCH_WITH_MCP - // All servers should connect in parallel, not sequentially - // With 5 servers and 5-second timeout each, sequential would take 25+ seconds - // Parallel should complete much faster - EXPECT_LT(duration, std::chrono::seconds(15)); - - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); - - auto composite = fetcher_->getComposite(); - // In real test: verify composite has all expected servers - // composite->servers().size() should equal server_count -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -TEST_F(ToolsFetcherIntegrationTest, LargeScaleParallelDiscovery) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - - // Test with many servers to stress parallel handling - const int server_count = 10; - createMockServers(server_count, 5000); - - std::string config = generateConfig(); - - auto result = waitForOperation([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }, 15); - -#ifdef GOPHER_ORCH_WITH_MCP - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); - - // Each server provides 3 tools, so total should be server_count * 3 - // (in a real test with working servers) - auto registry = fetcher_->getRegistry(); - // EXPECT_GE(registry->toolCount(), server_count * 3); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -// ============================================================================= -// Server Lifecycle Management Tests -// ============================================================================= - -TEST_F(ToolsFetcherIntegrationTest, ServerLifecycleManagement) { - GTEST_SKIP() << "Integration tests require actual MCP servers running"; - - // Test proper cleanup when servers go down - createMockServers(2); - - // Load initial configuration - std::string config1 = generateConfig(); - auto result1 = waitForOperation([this, &config1](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config1, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - auto registry1 = fetcher_->getRegistry(); - auto composite1 = fetcher_->getComposite(); - ASSERT_NE(registry1, nullptr); - ASSERT_NE(composite1, nullptr); - - // Simulate server going down - mock_servers_[0]->stop(); - - // Create new servers and reload - mock_servers_.clear(); - createMockServers(3, 6000); - - std::string config2 = generateConfig(); - auto result2 = waitForOperation([this, &config2](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config2, d, callback); - }); - - // Should have new registry and composite - auto registry2 = fetcher_->getRegistry(); - auto composite2 = fetcher_->getComposite(); - EXPECT_NE(registry2, nullptr); - EXPECT_NE(composite2, nullptr); - EXPECT_NE(registry1, registry2); - EXPECT_NE(composite1, composite2); -#else - EXPECT_TRUE(mcp::holds_alternative(result1)); -#endif -} \ No newline at end of file diff --git a/third_party/gopher-orch/tests/gopher/orch/tools_fetcher_test.cpp b/third_party/gopher-orch/tests/gopher/orch/tools_fetcher_test.cpp deleted file mode 100644 index a3bc77de..00000000 --- a/third_party/gopher-orch/tests/gopher/orch/tools_fetcher_test.cpp +++ /dev/null @@ -1,367 +0,0 @@ -// Unit tests for ToolsFetcher orchestration layer - -#include "gopher/orch/agent/tools_fetcher.h" - -#include -#include -#include -#include -#include -#include - -#include "gopher/orch/agent/config_loader.h" -#include "gopher/orch/agent/tool_registry.h" -#include "gopher/orch/server/mock_server.h" -#include "gopher/orch/server/server_composite.h" -#include "orch_test_fixture.h" -#include "gtest/gtest.h" - -using namespace gopher::orch::agent; -using namespace gopher::orch::server; - -// ============================================================================= -// ToolsFetcher Test Fixture -// ============================================================================= - -class ToolsFetcherTest : public OrchTest { - protected: - std::unique_ptr fetcher_; - - void SetUp() override { - OrchTest::SetUp(); - fetcher_ = std::make_unique(); - } - - // Helper to create a valid JSON config with MCP servers - std::string createValidConfig(int server_count = 2) { - std::string config = R"({ - "name": "test-config", - "mcp_servers": [)"; - - for (int i = 0; i < server_count; ++i) { - if (i > 0) config += ","; - config += R"( - { - "name": "server-)" + std::to_string(i) + R"(", - "transport": "http_sse", - "http_sse": { - "url": "http://localhost:)" + std::to_string(3000 + i) + R"(", - "headers": { - "Authorization": "Bearer test-key" - } - }, - "connect_timeout": 5000, - "request_timeout": 10000 - })"; - } - - config += R"( - ] - })"; - - return config; - } - - // Helper to wait for async operation - VoidResult waitForLoad(std::function)> operation) { - std::mutex mutex; - std::condition_variable cv; - bool done = false; - VoidResult result = VoidResult(Error(-1, "Not completed")); - - operation(*dispatcher_, [&](VoidResult r) { - std::lock_guard lock(mutex); - result = std::move(r); - done = true; - cv.notify_one(); - }); - - // Run dispatcher until done - auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); - while (!done && std::chrono::steady_clock::now() < timeout) { - dispatcher_->run(mcp::event::RunType::NonBlock); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - return result; - } -}; - -// ============================================================================= -// Basic Creation Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, CreateEmpty) { - EXPECT_EQ(fetcher_->getRegistry(), nullptr); - EXPECT_EQ(fetcher_->getComposite(), nullptr); -} - -// ============================================================================= -// JSON Configuration Parsing Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, LoadValidJsonConfig) { - std::string config = createValidConfig(2); - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - - // With MCP compiled in, this should create empty registry (servers won't connect in test) - // Without MCP, it should return error -#ifdef GOPHER_ORCH_WITH_MCP - EXPECT_TRUE(mcp::holds_alternative(result)); - - // Registry and composite should be created - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_EQ(mcp::get(result).message, "MCP support not compiled in"); -#endif -} - -TEST_F(ToolsFetcherTest, LoadEmptyServerList) { - std::string config = R"({ - "name": "test-config", - "mcp_servers": [] - })"; - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - // Should succeed with empty server list - EXPECT_TRUE(mcp::holds_alternative(result)); - - // Registry should be created but empty - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); - EXPECT_EQ(fetcher_->getRegistry()->toolCount(), 0u); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -// ============================================================================= -// Error Handling Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, LoadInvalidJson) { - std::string config = "{ invalid json"; - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_NE(mcp::get(result).message.find("parse"), std::string::npos); -} - -TEST_F(ToolsFetcherTest, LoadMissingRequiredFields) { - std::string config = R"({ - "name": "test-config" - })"; // Missing mcp_servers - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - - // Should handle missing fields gracefully -#ifdef GOPHER_ORCH_WITH_MCP - // ConfigLoader should provide empty server list if missing - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_NE(fetcher_->getRegistry(), nullptr); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -TEST_F(ToolsFetcherTest, LoadInvalidServerConfig) { - std::string config = R"({ - "name": "test-config", - "mcp_servers": [ - { - "name": "invalid-server" - } - ] - })"; // Missing transport config - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - // Should handle invalid server config - // The exact behavior depends on ConfigLoader validation - EXPECT_NE(fetcher_->getRegistry(), nullptr); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -// ============================================================================= -// File Loading Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, LoadFromFile) { - // Create temp file with config - const char* temp_file = "/tmp/test_tools_config.json"; - std::ofstream file(temp_file); - file << createValidConfig(1); - file.close(); - - auto result = waitForLoad([this, temp_file](Dispatcher& d, auto callback) { - fetcher_->loadFromFile(temp_file, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_NE(fetcher_->getRegistry(), nullptr); - EXPECT_NE(fetcher_->getComposite(), nullptr); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif - - // Clean up temp file - std::remove(temp_file); -} - -TEST_F(ToolsFetcherTest, LoadFromNonexistentFile) { - const char* nonexistent_file = "/tmp/does_not_exist_12345.json"; - - auto result = waitForLoad([this, nonexistent_file](Dispatcher& d, auto callback) { - fetcher_->loadFromFile(nonexistent_file, d, callback); - }); - - EXPECT_TRUE(mcp::holds_alternative(result)); - EXPECT_NE(mcp::get(result).message.find("Cannot open file"), std::string::npos); -} - -// ============================================================================= -// ServerComposite Creation Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, ServerCompositeCreated) { - std::string config = createValidConfig(3); - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto composite = fetcher_->getComposite(); - ASSERT_NE(composite, nullptr); - EXPECT_EQ(composite->name(), "ToolComposite"); - - // Servers should be added to composite (though not connected in test) - // Note: Without actual MCP servers running, we can't verify tool discovery -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -// ============================================================================= -// ToolRegistry Integration Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, ToolRegistryCreated) { - std::string config = createValidConfig(2); - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - EXPECT_TRUE(mcp::holds_alternative(result)); - - auto registry = fetcher_->getRegistry(); - ASSERT_NE(registry, nullptr); - - // Registry should be created but likely empty without real servers - // In a real environment, it would have tools from the MCP servers - EXPECT_GE(registry->toolCount(), 0u); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif -} - -// ============================================================================= -// Multiple Load Calls Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, MultipleLoadCalls) { - std::string config1 = createValidConfig(1); - std::string config2 = createValidConfig(2); - - // First load - auto result1 = waitForLoad([this, &config1](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config1, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - EXPECT_TRUE(mcp::holds_alternative(result1)); - auto registry1 = fetcher_->getRegistry(); - auto composite1 = fetcher_->getComposite(); - - // Second load should replace the previous config - auto result2 = waitForLoad([this, &config2](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config2, d, callback); - }); - - EXPECT_TRUE(mcp::holds_alternative(result2)); - auto registry2 = fetcher_->getRegistry(); - auto composite2 = fetcher_->getComposite(); - - // Should have new instances - EXPECT_NE(registry1, registry2); - EXPECT_NE(composite1, composite2); -#else - EXPECT_TRUE(mcp::holds_alternative(result1)); -#endif -} - -// ============================================================================= -// Environment Variable Tests -// ============================================================================= - -TEST_F(ToolsFetcherTest, EnvironmentVariableSubstitution) { - // Set an environment variable for the test - setenv("TEST_API_KEY", "secret-key-123", 1); - - std::string config = R"({ - "name": "test-config", - "mcp_servers": [ - { - "name": "server-with-env", - "transport": "http_sse", - "http_sse": { - "url": "http://localhost:3000", - "headers": { - "Authorization": "Bearer ${TEST_API_KEY}" - } - }, - "connect_timeout": 5000, - "request_timeout": 10000 - } - ] - })"; - - auto result = waitForLoad([this, &config](Dispatcher& d, auto callback) { - fetcher_->loadFromJson(config, d, callback); - }); - -#ifdef GOPHER_ORCH_WITH_MCP - EXPECT_TRUE(mcp::holds_alternative(result)); - // The ConfigLoader should handle environment variable substitution - EXPECT_NE(fetcher_->getRegistry(), nullptr); -#else - EXPECT_TRUE(mcp::holds_alternative(result)); -#endif - - // Clean up environment variable - unsetenv("TEST_API_KEY"); -} \ No newline at end of file diff --git a/third_party/gopher-orch/tests/orch/hello_test.cpp b/third_party/gopher-orch/tests/orch/hello_test.cpp deleted file mode 100644 index a6672772..00000000 --- a/third_party/gopher-orch/tests/orch/hello_test.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "orch/core/hello.h" - -#include -#include - -#include "orch/core/version.h" - -using namespace gopher::orch::core; -using namespace testing; - -class HelloTest : public ::testing::Test { - protected: - void SetUp() override { - // Setup code if needed - } - - void TearDown() override { - // Teardown code if needed - } -}; - -TEST_F(HelloTest, DefaultConstructor) { - Hello hello; - EXPECT_EQ(hello.greet(), "Hello, World!"); - EXPECT_EQ(hello.get_name(), "World"); -} - -TEST_F(HelloTest, ParameterizedConstructor) { - Hello hello("Alice"); - EXPECT_EQ(hello.greet(), "Hello, Alice!"); - EXPECT_EQ(hello.get_name(), "Alice"); -} - -TEST_F(HelloTest, SetName) { - Hello hello; - hello.set_name("Bob"); - EXPECT_EQ(hello.greet(), "Hello, Bob!"); - EXPECT_EQ(hello.get_name(), "Bob"); -} - -TEST_F(HelloTest, GreetWithPrefix) { - Hello hello("Charlie"); - EXPECT_EQ(hello.greet_with_prefix("Hi"), "Hi Charlie!"); - EXPECT_EQ(hello.greet_with_prefix("Welcome"), "Welcome Charlie!"); -} - -TEST_F(HelloTest, GetVersion) { EXPECT_EQ(Hello::get_version(), "0.1.0"); } - -TEST_F(HelloTest, EmptyName) { - Hello hello(""); - EXPECT_EQ(hello.greet(), "Hello, !"); - EXPECT_EQ(hello.get_name(), ""); -} - -TEST_F(HelloTest, SpecialCharacters) { - Hello hello("User@123!"); - EXPECT_EQ(hello.greet(), "Hello, User@123!!"); - EXPECT_EQ(hello.get_name(), "User@123!"); -} - -TEST_F(HelloTest, LongName) { - std::string long_name(1000, 'a'); - Hello hello(long_name); - EXPECT_EQ(hello.get_name(), long_name); - EXPECT_THAT(hello.greet(), StartsWith("Hello, ")); - EXPECT_THAT(hello.greet(), EndsWith("!")); -} - -// Test HelloBuilder -class HelloBuilderTest : public ::testing::Test { - protected: - HelloBuilder builder; -}; - -TEST_F(HelloBuilderTest, DefaultBuild) { - auto hello = builder.build(); - EXPECT_EQ(hello->greet(), "Hello, World!"); -} - -TEST_F(HelloBuilderTest, WithName) { - auto hello = builder.with_name("Diana").build(); - EXPECT_EQ(hello->greet(), "Hello, Diana!"); -} - -TEST_F(HelloBuilderTest, ChainedCalls) { - auto hello = builder.with_name("Eve").with_greeting_style("formal").build(); - EXPECT_EQ(hello->greet(), "Hello, Eve!"); -} - -TEST_F(HelloBuilderTest, MultipleBuildsSameBuilder) { - builder.with_name("Frank"); - auto hello1 = builder.build(); - auto hello2 = builder.build(); - - EXPECT_EQ(hello1->greet(), "Hello, Frank!"); - EXPECT_EQ(hello2->greet(), "Hello, Frank!"); - - // Verify they are independent objects - hello1->set_name("George"); - EXPECT_EQ(hello1->get_name(), "George"); - EXPECT_EQ(hello2->get_name(), "Frank"); -} - -// Version tests -TEST(VersionTest, VersionConstants) { - EXPECT_EQ(Version::major(), 0); - EXPECT_EQ(Version::minor(), 1); - EXPECT_EQ(Version::patch(), 0); - EXPECT_STREQ(Version::string(), "0.1.0"); -} diff --git a/third_party/gopher-orch/third_party/gopher-mcp b/third_party/gopher-orch/third_party/gopher-mcp deleted file mode 160000 index 046c7879..00000000 --- a/third_party/gopher-orch/third_party/gopher-mcp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 046c7879d2f4a51c4cdf4b19ec92e812b43bac22