diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0678f020..d05e462e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,15 @@ jobs: source ./bin/activate-hermit export CARGO_INCREMENTAL=0 cargo clippy --jobs 2 -- -D warnings + + - name: Install Node.js Dependencies for OpenAPI Check + run: source ../../bin/activate-hermit && npm ci + working-directory: ui/desktop + + - name: Check OpenAPI Schema is Up-to-Date + run: | + source ./bin/activate-hermit + just check-openapi-schema desktop-lint: name: Lint Electron Desktop App @@ -147,6 +156,7 @@ jobs: run: source ../../bin/activate-hermit && npm run lint:check working-directory: ui/desktop + # Faster Desktop App build for PRs only bundle-desktop-unsigned: uses: ./.github/workflows/bundle-desktop.yml diff --git a/Justfile b/Justfile index dbe99a53..4625b8bb 100644 --- a/Justfile +++ b/Justfile @@ -184,6 +184,10 @@ run-server: @echo "Running server..." cargo run -p goose-server +# Check if OpenAPI schema is up-to-date +check-openapi-schema: generate-openapi + ./scripts/check-openapi-schema.sh + # Generate OpenAPI specification without starting the UI generate-openapi: @echo "Generating OpenAPI schema..." @@ -368,16 +372,16 @@ set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] ### Build the core code ### profile = --release or "" for debug ### allparam = OR/AND/ANY/NONE --workspace --all-features --all-targets -win-bld profile allparam: +win-bld profile allparam: cargo run {{profile}} -p goose-server --bin generate_schema cargo build {{profile}} {{allparam}} ### Build just debug -win-bld-dbg: +win-bld-dbg: just win-bld " " " " ### Build debug and test, examples,... -win-bld-dbg-all: +win-bld-dbg-all: just win-bld " " "--workspace --all-targets --all-features" ### Build just release @@ -440,8 +444,8 @@ win-total-rls *allparam: just win-bld-rls{{allparam}} just win-run-rls -### Build and run the Kotlin example with -### auto-generated bindings for goose-llm +### Build and run the Kotlin example with +### auto-generated bindings for goose-llm kotlin-example: # Build Rust dylib and generate Kotlin bindings cargo build -p goose-llm @@ -460,4 +464,3 @@ kotlin-example: -Djna.library.path=$HOME/Development/goose/target/debug \ -classpath "example.jar:libs/kotlin-stdlib-1.9.0.jar:libs/kotlinx-coroutines-core-jvm-1.7.3.jar:libs/jna-5.13.0.jar" \ UsageKt - diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index bc43201d..46170409 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -393,6 +393,8 @@ derive_utoipa!(Annotations as AnnotationsSchema); goose::recipe::RecipeParameterRequirement, goose::recipe::Response, goose::recipe::SubRecipe, + goose::agents::types::RetryConfig, + goose::agents::types::SuccessCheck, )) )] pub struct ApiDoc; diff --git a/crates/goose/src/agents/types.rs b/crates/goose/src/agents/types.rs index ccfb5deb..10054d16 100644 --- a/crates/goose/src/agents/types.rs +++ b/crates/goose/src/agents/types.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::{mpsc, Mutex}; +use utoipa::ToSchema; /// Type alias for the tool result channel receiver pub type ToolResultReceiver = Arc>)>>>; @@ -16,7 +17,7 @@ pub const DEFAULT_RETRY_TIMEOUT_SECONDS: u64 = 300; pub const DEFAULT_ON_FAILURE_TIMEOUT_SECONDS: u64 = 600; /// Configuration for retry logic in recipe execution -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RetryConfig { /// Maximum number of retry attempts before giving up pub max_retries: u32, @@ -59,7 +60,7 @@ impl RetryConfig { } /// A single success check to validate recipe completion -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type")] pub enum SuccessCheck { /// Execute a shell command and check its exit status diff --git a/scripts/check-openapi-schema.sh b/scripts/check-openapi-schema.sh new file mode 100755 index 00000000..cbf07ac8 --- /dev/null +++ b/scripts/check-openapi-schema.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +# Check if OpenAPI schema is up-to-date +# This script generates the OpenAPI schema and compares it with the committed version + +echo "🔍 Checking OpenAPI schema is up-to-date..." + +# Check if the generated schema differs from the committed version +echo "🔍 Comparing generated schema with committed version..." +if ! git diff --exit-code ui/desktop/openapi.json ui/desktop/src/api/; then + echo "" + echo "❌ OpenAPI schema is out of date!" + echo "" + echo "The generated OpenAPI schema differs from the committed version." + echo "This usually means that API types were added or modified without updating the schema." + echo "" + echo "To fix this issue:" + echo "1. Run 'just generate-openapi' locally" + echo "2. Commit the changes to ui/desktop/openapi.json and ui/desktop/src/api/" + echo "3. Push your changes" + echo "" + echo "Changes detected:" + git diff ui/desktop/openapi.json ui/desktop/src/api/ + exit 1 +fi + +echo "✅ OpenAPI schema is up-to-date" diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 332ca65d..971f05db 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -2183,7 +2183,7 @@ }, "Recipe": { "type": "object", - "description": "A Recipe represents a personalized, user-generated agent configuration that defines\nspecific behaviors and capabilities within the Goose system.\n\n# Fields\n\n## Required Fields\n* `version` - Semantic version of the Recipe file format (defaults to \"1.0.0\")\n* `title` - Short, descriptive name of the Recipe\n* `description` - Detailed description explaining the Recipe's purpose and functionality\n* `Instructions` - Instructions that defines the Recipe's behavior\n\n## Optional Fields\n* `prompt` - the initial prompt to the session to start with\n* `extensions` - List of extension configurations required by the Recipe\n* `context` - Supplementary context information for the Recipe\n* `activities` - Activity labels that appear when loading the Recipe\n* `author` - Information about the Recipe's creator and metadata\n* `parameters` - Additional parameters for the Recipe\n* `response` - Response configuration including JSON schema validation\n\n# Example\n\n\nuse goose::recipe::Recipe;\n\n// Using the builder pattern\nlet recipe = Recipe::builder()\n.title(\"Example Agent\")\n.description(\"An example Recipe configuration\")\n.instructions(\"Act as a helpful assistant\")\n.build()\n.expect(\"Missing required fields\");\n\n// Or using struct initialization\nlet recipe = Recipe {\nversion: \"1.0.0\".to_string(),\ntitle: \"Example Agent\".to_string(),\ndescription: \"An example Recipe configuration\".to_string(),\ninstructions: Some(\"Act as a helpful assistant\".to_string()),\nprompt: None,\nextensions: None,\ncontext: None,\nactivities: None,\nauthor: None,\nsettings: None,\nparameters: None,\nresponse: None,\nsub_recipes: None,\n};\n", + "description": "A Recipe represents a personalized, user-generated agent configuration that defines\nspecific behaviors and capabilities within the Goose system.\n\n# Fields\n\n## Required Fields\n* `version` - Semantic version of the Recipe file format (defaults to \"1.0.0\")\n* `title` - Short, descriptive name of the Recipe\n* `description` - Detailed description explaining the Recipe's purpose and functionality\n* `Instructions` - Instructions that defines the Recipe's behavior\n\n## Optional Fields\n* `prompt` - the initial prompt to the session to start with\n* `extensions` - List of extension configurations required by the Recipe\n* `context` - Supplementary context information for the Recipe\n* `activities` - Activity labels that appear when loading the Recipe\n* `author` - Information about the Recipe's creator and metadata\n* `parameters` - Additional parameters for the Recipe\n* `response` - Response configuration including JSON schema validation\n* `retry` - Retry configuration for automated validation and recovery\n# Example\n\n\nuse goose::recipe::Recipe;\n\n// Using the builder pattern\nlet recipe = Recipe::builder()\n.title(\"Example Agent\")\n.description(\"An example Recipe configuration\")\n.instructions(\"Act as a helpful assistant\")\n.build()\n.expect(\"Missing required fields\");\n\n// Or using struct initialization\nlet recipe = Recipe {\nversion: \"1.0.0\".to_string(),\ntitle: \"Example Agent\".to_string(),\ndescription: \"An example Recipe configuration\".to_string(),\ninstructions: Some(\"Act as a helpful assistant\".to_string()),\nprompt: None,\nextensions: None,\ncontext: None,\nactivities: None,\nauthor: None,\nsettings: None,\nparameters: None,\nresponse: None,\nsub_recipes: None,\nretry: None,\n};\n", "required": [ "title", "description" @@ -2244,6 +2244,14 @@ ], "nullable": true }, + "retry": { + "allOf": [ + { + "$ref": "#/components/schemas/RetryConfig" + } + ], + "nullable": true + }, "settings": { "allOf": [ { @@ -2373,6 +2381,48 @@ } } }, + "RetryConfig": { + "type": "object", + "description": "Configuration for retry logic in recipe execution", + "required": [ + "max_retries", + "checks" + ], + "properties": { + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SuccessCheck" + }, + "description": "List of success checks to validate recipe completion" + }, + "max_retries": { + "type": "integer", + "format": "int32", + "description": "Maximum number of retry attempts before giving up", + "minimum": 0 + }, + "on_failure": { + "type": "string", + "description": "Optional shell command to run on failure for cleanup", + "nullable": true + }, + "on_failure_timeout_seconds": { + "type": "integer", + "format": "int64", + "description": "Timeout in seconds for on_failure commands (default: 600 seconds)", + "nullable": true, + "minimum": 0 + }, + "timeout_seconds": { + "type": "integer", + "format": "int64", + "description": "Timeout in seconds for individual shell commands (default: 300 seconds)", + "nullable": true, + "minimum": 0 + } + } + }, "Role": { "oneOf": [ { @@ -2685,6 +2735,34 @@ } } }, + "SuccessCheck": { + "oneOf": [ + { + "type": "object", + "description": "Execute a shell command and check its exit status", + "required": [ + "command", + "type" + ], + "properties": { + "command": { + "type": "string", + "description": "The shell command to execute" + }, + "type": { + "type": "string", + "enum": [ + "Shell" + ] + } + } + } + ], + "description": "A single success check to validate recipe completion", + "discriminator": { + "propertyName": "type" + } + }, "SummarizationRequested": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index dbb28b82..53230d06 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -396,7 +396,7 @@ export type ProvidersResponse = { * * `author` - Information about the Recipe's creator and metadata * * `parameters` - Additional parameters for the Recipe * * `response` - Response configuration including JSON schema validation - * + * * `retry` - Retry configuration for automated validation and recovery * # Example * * @@ -425,6 +425,7 @@ export type ProvidersResponse = { * parameters: None, * response: None, * sub_recipes: None, + * retry: None, * }; * */ @@ -438,6 +439,7 @@ export type Recipe = { parameters?: Array | null; prompt?: string | null; response?: Response | null; + retry?: RetryConfig | null; settings?: Settings | null; sub_recipes?: Array | null; title: string; @@ -474,6 +476,32 @@ export type Response = { json_schema?: unknown; }; +/** + * Configuration for retry logic in recipe execution + */ +export type RetryConfig = { + /** + * List of success checks to validate recipe completion + */ + checks: Array; + /** + * Maximum number of retry attempts before giving up + */ + max_retries: number; + /** + * Optional shell command to run on failure for cleanup + */ + on_failure?: string | null; + /** + * Timeout in seconds for on_failure commands (default: 600 seconds) + */ + on_failure_timeout_seconds?: number | null; + /** + * Timeout in seconds for individual shell commands (default: 300 seconds) + */ + timeout_seconds?: number | null; +}; + export type Role = string; export type RunNowResponse = { @@ -602,6 +630,17 @@ export type SubRecipe = { } | null; }; +/** + * Execute a shell command and check its exit status + */ +export type SuccessCheck = { + /** + * The shell command to execute + */ + command: string; + type: 'Shell'; +}; + export type SummarizationRequested = { msg: string; };