diff --git a/.github/DISCUSSION_TEMPLATE/qa.yml b/.github/DISCUSSION_TEMPLATE/qa.yml
new file mode 100644
index 0000000000..9bbd676aff
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/qa.yml
@@ -0,0 +1,26 @@
+title: "❓ Question: [Brief summary]"
+labels:
+ - help
+body:
+ - type: markdown
+ attributes:
+ value: |
+ 💡 Before posting, please attach your **diagnostics zip** — it helps the Goose team debug faster and saves everyone time.
+ [How to capture and share diagnostics](https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting/)
+ - type: textarea
+ id: problem
+ attributes:
+ label: What happened?
+ description: Describe the issue in detail and attach your diagnostics zip if possible.
+ validations:
+ required: true
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ description: Tell us how to reproduce the issue — commands, steps, or context.
+ - type: textarea
+ id: version
+ attributes:
+ label: Goose version and environment
+ description: Include your Goose version and operating system if known.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 689e3ef170..e2d4d93e0e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -2,17 +2,22 @@
name: Bug report
about: Create a report to help us improve
title: ''
-labels: ''
+labels: bug
assignees: ''
-
---
**Describe the bug**
-Note: Please check the common issues on https://block.github.io/goose/docs/troubleshooting before filing a report
+💡 Before filing, please check common issues:
+https://block.github.io/goose/docs/troubleshooting
+
+📦 To help us debug faster, attach your **diagnostics zip** if possible.
+👉 How to capture it: https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting/
A clear and concise description of what the bug is.
+---
+
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
@@ -20,18 +25,26 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
+---
+
**Expected behavior**
A clear and concise description of what you expected to happen.
+---
+
**Screenshots**
If applicable, add screenshots to help explain your problem.
-**Please provide following information:**
- - **OS & Arch:** [e.g. Ubuntu 22.04 x86]
- - **Interface:** [UI/CLI]
- - **Version:** [e.g. v1.0.2]
- - **Extensions enabled:** [e.g. Computer Controller, Figma]
- - **Provider & Model:** [e.g. Google - gemini-1.5-pro]
+---
+
+**Please provide the following information**
+- **OS & Arch:** [e.g. Ubuntu 22.04 x86]
+- **Interface:** [UI / CLI]
+- **Version:** [e.g. v1.0.2]
+- **Extensions enabled:** [e.g. Computer Controller, Figma]
+- **Provider & Model:** [e.g. Google – gemini-1.5-pro]
+
+---
**Additional context**
Add any other context about the problem here.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index b8f1f7e29f..bc90ec22d9 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -14,6 +14,10 @@
- [ ] Build / Release
- [ ] Other (specify below)
+### AI Assistance
+
+- [ ] This PR was created or reviewed with AI assistance
+
### Testing
@@ -27,5 +31,6 @@ Before:
After:
-
+### Submitting a Recipe?
+
**Email**:
diff --git a/.github/workflows/bundle-desktop-manual.yml b/.github/workflows/bundle-desktop-manual.yml
new file mode 100644
index 0000000000..f2943e8d66
--- /dev/null
+++ b/.github/workflows/bundle-desktop-manual.yml
@@ -0,0 +1,19 @@
+name: Manual Desktop Bundle (Unsigned)
+
+on:
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: 'Branch name to bundle app from'
+ required: true
+ type: string
+
+jobs:
+ bundle-desktop-unsigned:
+ uses: ./.github/workflows/bundle-desktop.yml
+ permissions:
+ id-token: write
+ contents: read
+ with:
+ signing: false
+ ref: ${{ inputs.branch }}
diff --git a/.github/workflows/bundle-desktop.yml b/.github/workflows/bundle-desktop.yml
index 8995eb92c6..978242a621 100644
--- a/.github/workflows/bundle-desktop.yml
+++ b/.github/workflows/bundle-desktop.yml
@@ -3,6 +3,7 @@
# - release.yml
# - canary.yml
# - pr-comment-bundle-desktop.yml
+# - bundle-desktop-manual.yml
on:
workflow_call:
inputs:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 172b02d30e..4242db3718 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -131,13 +131,4 @@ jobs:
run: source ../../bin/activate-hermit && npm run test:run
working-directory: ui/desktop
- # Faster Desktop App build for PRs only
- bundle-desktop-unsigned:
- uses: ./.github/workflows/bundle-desktop.yml
- permissions:
- id-token: write
- contents: read
- needs: changes
- if: (github.event_name == 'pull_request' || github.event_name == 'merge_group') && (needs.changes.outputs.code == 'true' || github.event_name != 'pull_request')
- with:
- signing: false
+
diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml
index 84a0e69647..e301804b03 100644
--- a/.github/workflows/pr-smoke-test.yml
+++ b/.github/workflows/pr-smoke-test.yml
@@ -2,6 +2,9 @@ on:
pull_request:
branches:
- main
+ push:
+ branches:
+ - main
workflow_dispatch:
inputs:
branch:
@@ -13,8 +16,16 @@ on:
name: Live Provider Tests
jobs:
+ check-fork:
+ runs-on: ubuntu-latest
+ # Skip entire workflow for PRs from forks (they don't have access to secrets)
+ if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
+ steps:
+ - run: echo "Not a fork PR - proceeding with smoke tests"
+
changes:
runs-on: ubuntu-latest
+ needs: check-fork
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
@@ -117,6 +128,21 @@ jobs:
# Run the provider test script (binary already built and downloaded)
bash scripts/test_providers.sh
+ - name: Run MCP Tests
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
+ DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
+ DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
+ TETRATE_API_KEY: ${{ secrets.TETRATE_API_KEY }}
+ HOME: /tmp/goose-home
+ GOOSE_DISABLE_KEYRING: 1
+ SKIP_BUILD: 1
+ run: |
+ bash scripts/test_mcp.sh
+
- name: Run Subrecipe Tests
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3def3d3627..7ec7a08c82 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,23 +9,7 @@ We welcome pull requests for general contributions! If you have a larger new fea
---
-## 🎉 Hacktoberfest 2025 🎉
-
-`goose` is a participating in Hacktoberfest 2025! We’re so excited for your contributions, and have created a wide variety of issues so that anyone can contribute. Whether you're a seasoned developer or a first-time open source contributor, there's something for everyone.
-
-### Here's how you can get started:
-
-1. Read the [code of conduct](https://github.com/block/.github/blob/main/CODE_OF_CONDUCT.md).
-2. Skim the quick AI contribution tips below (and see the [full Responsible AI-Assisted Coding Guide](./ai-assisted-coding-guide.md) for details).
-3. Choose a task from this project's Hacktoberfest issues in our [Project Hub](https://github.com/block/goose/issues/4705). Each issue has the 🏷️ `hacktoberfest` label.
-4. Comment ".take" on the corresponding issue to get assigned the task.
-5. Fork the repository and create a new branch for your work.
-6. Make your changes and submit a pull request.
-7. Wait for review and address any feedback.
-
----
-
-### 🤖 Quick Responsible AI Tips
+## 🤖 Quick Responsible AI Tips
If you use Goose, Copilot, Claude, or other AI tools to help with your PRs:
@@ -51,35 +35,7 @@ If you use Goose, Copilot, Claude, or other AI tools to help with your PRs:
- Document your changes
- Ask for review if security or core code is involved
-👉 Full guide here: [Responsible AI-Assisted Coding Guide](./ai-assisted-coding-guide.md)
-
----
-
-### 🏆 Leaderboard & Prizes
-
-Every hacktoberfest PR and contribution will earn you points on our [leaderboard](https://github.com/block/goose/issues/4775). Those who end up in the top 20 participants with the most points by the end of October will earn exclusive swag and LLM credits! As you have issues merged, here is a brief explanation on how our automatic points system works.
-
-#### Point System
-
-| Weight | Points Awarded | Description |
-|---------|-------------|-------------|
-| 🐭 **Small** | 5 points | For smaller tasks that take limited time to complete and/or don't require any product knowledge. |
-| 🐰 **Medium** | 10 points | For average tasks that take additional time to complete and/or require some product knowledge. |
-| 🐂 **Large** | 15 points | For heavy tasks that takes lots of time to complete and/or possibly require deep product knowledge. |
-
-#### Prizes You Can Win
-
-- **Top 5**: $100 gift card to our [brand new goose swag shop](https://www.gooseswag.xyz/) and $100 of LLM credits!
-- **Top 6-10**: $50 gift cards for goose swag shop and $50 of LLM credits!
-- **Top 11-20**: $25 of LLM credits!
-
-Keep an eye on your progress via our [Leaderboard](https://github.com/block/goose/issues/4775).
-
-### 👩 Need help?
-
-Need help or have questions? Feel free to reach out by connecting with us in our [Discord community](https://discord.gg/goose-oss) to get direct help from our team in the `#hacktoberfest` project channel.
-
-Happy contributing!
+👉 Full guide here: [Responsible AI-Assisted Coding Guide](./HOWTOAI.md)
---
diff --git a/Cargo.lock b/Cargo.lock
index 02ebf79e7a..7c35094641 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2667,6 +2667,7 @@ dependencies = [
"fs2",
"futures",
"include_dir",
+ "indexmap 2.12.0",
"indoc",
"insta",
"jsonschema",
@@ -2938,7 +2939,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"slab",
"tokio",
"tokio-util",
@@ -2957,7 +2958,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.2.0",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"slab",
"tokio",
"tokio-util",
@@ -3001,6 +3002,12 @@ dependencies = [
"foldhash",
]
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+
[[package]]
name = "hashlink"
version = "0.8.4"
@@ -3558,13 +3565,14 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.7.1"
+version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
- "hashbrown 0.15.2",
+ "hashbrown 0.16.0",
"serde",
+ "serde_core",
]
[[package]]
@@ -4016,7 +4024,7 @@ dependencies = [
"chrono",
"encoding_rs",
"flate2",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"itoa",
"log",
"md-5",
@@ -4919,7 +4927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"quick-xml 0.32.0",
"serde",
"time",
@@ -5072,7 +5080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d35f4dc9988d1326b065b4def5e950c3ed727aa03e3151b86cc9e2aec6b03f54"
dependencies = [
"futures",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"nix 0.29.0",
"tokio",
"tracing",
@@ -6039,7 +6047,7 @@ version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"itoa",
"memchr",
"ryu",
@@ -6088,7 +6096,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"serde",
"serde_derive",
"serde_json",
@@ -6114,7 +6122,7 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"itoa",
"ryu",
"serde",
@@ -6361,7 +6369,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"log",
"memchr",
"once_cell",
@@ -7125,7 +7133,7 @@ version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -7609,7 +7617,7 @@ version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23"
dependencies = [
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"serde",
"serde_json",
"utoipa-gen",
@@ -8726,7 +8734,7 @@ dependencies = [
"crc32fast",
"crossbeam-utils",
"flate2",
- "indexmap 2.7.1",
+ "indexmap 2.12.0",
"memchr",
"zopfli",
]
diff --git a/Cargo.toml b/Cargo.toml
index 6c312ee8c8..d88b69afed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ description = "An AI agent"
[workspace.lints.clippy]
uninlined_format_args = "allow"
+string_slice = "warn"
[workspace.dependencies]
rmcp = { version = "0.8.3", features = ["schemars", "auth"] }
diff --git a/HOWTOAI.md b/HOWTOAI.md
new file mode 100644
index 0000000000..32efe2cb71
--- /dev/null
+++ b/HOWTOAI.md
@@ -0,0 +1,317 @@
+# How to Use AI with goose
+_A practical guide for contributing to goose using AI coding assistants_
+
+goose benefits from thoughtful AI-assisted development, but contributors must maintain high standards for code quality, security, and collaboration. Whether you use goose itself, GitHub Copilot, Cursor, Claude, or other AI tools, this guide will help you contribute effectively.
+
+---
+
+## Core Principles
+
+- **Human Oversight**: You are accountable for all code you submit. Never commit code you don’t understand or can’t maintain.
+- **Quality Standards**: AI code must meet the same standards as human written code—tests, docs, and patterns included.
+- **Transparency**: Be open about significant AI usage in PRs and explain how you validated it.
+
+---
+
+## Best Practices
+
+**✅ Recommended Uses**
+
+- Generating boilerplate code and common patterns
+- Creating comprehensive test suites
+- Writing documentation and comments
+- Refactoring existing code for clarity
+- Generating utility functions and helpers
+- Explaining existing code patterns
+
+**❌ Avoid AI For**
+
+- Complex business logic without thorough review
+- Security critical authentication/authorization code
+- Code you don’t fully understand
+- Large architectural changes
+- Database migrations or schema changes
+
+**Workflow Tips**
+
+- Start small and validate often. Build, lint, and test incrementally
+- Study existing patterns before generating new code
+- Always ask: "Is this secure? Does it follow project patterns? What edge cases need testing?"
+
+**Security Considerations**
+
+- Extra review required for MCP servers, network code, file system ops, user input, and credential handling
+- Never expose secrets in prompts
+- Sanitize inputs/outputs and follow goose’s security patterns
+
+---
+
+## Testing & Review
+
+Before submitting AI assisted code, confirm that:
+- You understand every line
+- All tests pass locally (happy path + error cases)
+- Docs are updated and accurate
+- Code follows existing patterns
+
+**Always get human review** for:
+
+- Security sensitive code
+- Core architecture changes
+- Async/concurrency logic
+- MCP protocol implementations
+- Large refactors or anything you’re unsure about
+
+---
+
+## Using goose for goose development
+
+- Protect sensitive files with `.gooseignore` (e.g., `.env*`, `*.key`, `target/`, `.git/`)
+- Guide goose with `.goosehints` (patterns, error handling, formatting, tests, docs)
+- Use `/plan` to structure work, and choose modes wisely:
+ - **Chat** for understanding
+ - **Smart Approval** for most dev work
+ - **Approval** for critical areas
+ - **Autonomous** only with safety nets
+
+---
+
+## Community & Collaboration
+
+- In PRs, note significant AI use and how you validated results
+- Share prompting tips, patterns, and pitfalls
+- Be responsive to feedback and help improve this guide
+
+---
+
+## Remember
+
+AI is a powerful assistant, not a replacement for your judgment. Use it to speed up development; while keeping your brain engaged, your standards high, and goose secure.
+
+Questions? Join our [Discord](https://discord.gg/goose-oss) or [GitHub Discussions](https://github.com/block/goose/discussions) to talk more about responsible AI development.
+
+---
+
+## Getting Started with AI Tools
+
+### Quick Setup
+
+**Using goose (meta!):**
+```bash
+# Install goose
+curl -fsSL https://github.com/block/goose/releases/latest/download/install.sh | bash
+
+# Navigate to your goose clone
+cd /path/to/goose
+
+# Start goose in the repo
+goose
+```
+
+**Using GitHub Copilot:**
+- Install the [GitHub Copilot extension](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) for VS Code
+- Enable Copilot for Rust files in your settings
+- Recommended: Also install [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) for better code intelligence
+
+**Using Cursor:**
+- Download [Cursor](https://cursor.sh/) (VS Code fork with built-in AI)
+- Open the goose repository
+- Use Cmd/Ctrl+K for inline AI editing, Cmd/Ctrl+L for chat
+
+**Using Claude or ChatGPT:**
+- Copy relevant code sections into the chat interface
+- Provide context about the goose architecture (see below)
+- Always test generated code locally before committing
+
+### Rust-Specific Configuration
+
+If you're new to Rust, configure your AI tool to help you learn:
+
+**VS Code settings.json:**
+```json
+{
+ "rust-analyzer.checkOnSave.command": "clippy",
+ "github.copilot.enable": {
+ "rust": true
+ }
+}
+```
+
+**Cursor Rules (.cursorrules in repo root):**
+```
+This is a Rust project using cargo workspaces.
+- Follow existing error handling patterns using anyhow::Result
+- Use async/await for I/O operations
+- Follow the project's clippy lints (see clippy-baselines/)
+- Run cargo fmt before committing
+```
+
+---
+
+## Understanding goose's Architecture
+
+New to AI agents? Here are key questions to ask your AI tool:
+
+### Essential Concepts
+
+**"Explain the goose crate structure"**
+```
+Ask: "I'm looking at the goose repository. Can you explain the purpose of each crate
+in the crates/ directory and how they relate to each other?"
+
+Key insight: goose uses a workspace with specialized crates:
+- goose: Core agent logic
+- goose-cli: Command-line interface
+- goose-server: Backend for desktop app (goosed)
+- goose-mcp: MCP server implementations
+```
+
+**"How does the MCP protocol work in goose?"**
+```
+Ask: "What is the Model Context Protocol (MCP) and how does goose implement it?
+Show me an example from crates/goose-mcp/"
+
+Key insight: MCP allows goose to connect to external tools and data sources.
+Each MCP server provides specific capabilities (developer tools, file access, etc.)
+```
+
+**"What's the agent execution flow?"**
+```
+Ask: "Walk me through what happens when a user sends a message to goose.
+Start from crates/goose-cli/src/main.rs"
+
+Key insight: Message → Agent → Provider (LLM) → Tool execution → Response
+```
+
+### Navigating the Codebase with AI
+
+**Finding the right file:**
+```
+# Use ripgrep with AI assistance
+Ask: "I want to add a new shell command tool. Where should I look?"
+AI might suggest: rg "shell" crates/goose-mcp/ -l
+
+Then ask: "Explain the structure of crates/goose-mcp/src/developer/tools/shell.rs"
+```
+
+**Understanding patterns:**
+```
+Ask: "Show me the pattern for implementing a new Provider in goose"
+Then: "What's the difference between streaming and non-streaming providers?"
+```
+
+---
+
+## Practical Examples
+
+### Example 1: Understanding How to Add a New MCP Tool
+
+**Scenario:** You want to add a new tool to the developer MCP server.
+
+**Step 1 - Explore existing tools:**
+```bash
+# Ask AI: "Show me the structure of an existing MCP tool"
+ls crates/goose-mcp/src/developer/tools/
+
+# Pick a simple one to study
+# Ask AI: "Explain this tool implementation line by line"
+cat crates/goose-mcp/src/developer/tools/shell.rs
+```
+
+**Step 2 - Ask AI to draft your new tool:**
+```
+Prompt: "I want to add a new MCP tool called 'git_status' that runs git status
+and returns the output. Based on the pattern in shell.rs, draft the implementation."
+```
+
+**Step 3 - Validate with AI:**
+```
+Ask: "Review this code for:
+1. Proper error handling using anyhow::Result
+2. Security concerns (command injection, etc.)
+3. Async/await patterns matching the codebase
+4. Test coverage needs"
+```
+
+**Step 4 - Test locally:**
+```bash
+# Build and test
+cargo build -p goose-mcp
+cargo test -p goose-mcp
+
+# Run clippy
+./scripts/clippy-lint.sh
+```
+
+### Example 2: Fixing a Rust Compiler Error
+
+**Scenario:** You're getting a lifetime error you don't understand.
+
+**Step 1 - Copy the full error:**
+```bash
+cargo build 2>&1 | pbcopy # macOS
+cargo build 2>&1 | xclip # Linux
+```
+
+**Step 2 - Ask AI with context:**
+```
+Prompt: "I'm getting this Rust compiler error in the goose project:
+
+[paste error]
+
+Here's the relevant code:
+[paste code section]
+
+Explain what's wrong and how to fix it following Rust best practices."
+```
+
+**Step 3 - Understand the fix:**
+```
+Ask: "Explain why this fix works and what I should learn about Rust lifetimes"
+```
+
+**Step 4 - Apply and verify:**
+```bash
+# Apply the fix
+# Then verify it compiles and tests pass
+cargo build
+cargo test
+```
+
+### Example 3: Adding a Feature to the CLI
+
+**Scenario:** You want to add a new command-line flag to goose-cli.
+
+**Step 1 - Find the CLI argument parsing:**
+```bash
+# Ask AI: "Where does goose-cli parse command line arguments?"
+rg "clap" crates/goose-cli/src/ -l
+```
+
+**Step 2 - Study the pattern:**
+```
+Ask: "Explain how goose-cli uses clap for argument parsing.
+Show me how existing flags are defined."
+```
+
+**Step 3 - Draft your addition:**
+```
+Prompt: "I want to add a --verbose flag that enables debug logging.
+Based on the existing patterns in goose-cli, show me:
+1. How to add the flag to the CLI args struct
+2. How to pass it to the goose core
+3. How to use it to control log levels"
+```
+
+**Step 4 - Implement with validation:**
+```bash
+# Make changes
+# Build both crates
+cargo build -p goose-cli -p goose
+
+# Test the new flag
+./target/debug/goose --verbose session
+
+# Run tests
+cargo test -p goose-cli
+```
\ No newline at end of file
diff --git a/README.md b/README.md
index db7b7f55dd..de9bdde7f2 100644
--- a/README.md
+++ b/README.md
@@ -17,20 +17,6 @@ _a local, extensible, open source AI agent that automates engineering tasks_
-## 🎉 Hacktoberfest 2025 🎉
-
-`goose` is a participating project in Hacktoberfest 2025! We’re so excited for your contributions, and have created a wide variety of issues so that anyone can contribute. Whether you're a seasoned developer or a first-time open source contributor, there's something for everyone.
-
-### To get started:
-1. Read the [contributing guide](https://github.com/block/goose/blob/main/CONTRIBUTING.md).
-2. Read the [code of conduct](https://github.com/block/.github/blob/main/CODE_OF_CONDUCT.md).
-3. Read the [full Responsible AI-Assisted Coding Guide](./ai-assisted-coding-guide.md).
-4. Choose a task from this project's Hacktoberfest issues in our [Project Hub](https://github.com/block/goose/issues/4705) and follow the instructions. Each issue has the 🏷️ `hacktoberfest` label.
-
-Have questions? Connecting with us in our [Discord community](https://discord.gg/goose-oss) in the `#hacktoberfest` project channel.
-
----
-
goose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_.
Whether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.
@@ -44,8 +30,13 @@ Designed for maximum flexibility, goose works with any LLM and supports multi-mo
- [Installation](https://block.github.io/goose/docs/getting-started/installation)
- [Tutorials](https://block.github.io/goose/docs/category/tutorials)
- [Documentation](https://block.github.io/goose/docs/category/getting-started)
+- [Responsible AI-Assisted Coding Guide](https://github.com/block/goose/blob/main/HOWTOAI.md)
- [Governance](https://github.com/block/goose/blob/main/GOVERNANCE.md)
+## Need Help?
+- [Diagnostics & Reporting](https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting)
+- [Known Issues](https://block.github.io/goose/docs/troubleshooting/known-issues)
+
# a little goose humor 🦢
> Why did the developer choose goose as their AI agent?
diff --git a/ai-assisted-coding-guide.md b/ai-assisted-coding-guide.md
deleted file mode 100644
index 3464f0bb4d..0000000000
--- a/ai-assisted-coding-guide.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Responsible AI-Assisted Coding Guide
-_Guidelines for contributing responsibly to goose during Hacktoberfest_
-
-goose benefits from thoughtful AI assisted development, but contributors must maintain high standards for code quality, security, and collaboration. Whether you use goose, Copilot, Claude, or other AI tools, these principles will help you avoid common pitfalls.
-
----
-
-## Core Principles
-
-- **Human Oversight**: You are accountable for all code you submit. Never commit code you don’t understand or can’t maintain.
-- **Quality Standards**: AI code must meet the same standards as human written code—tests, docs, and patterns included.
-- **Transparency**: Be open about significant AI usage in PRs and explain how you validated it.
-
----
-
-## Best Practices
-
-**✅ Recommended Uses**
-
-- Generating boilerplate code and common patterns
-- Creating comprehensive test suites
-- Writing documentation and comments
-- Refactoring existing code for clarity
-- Generating utility functions and helpers
-- Explaining existing code patterns
-
-**❌ Avoid AI For**
-
-- Complex business logic without thorough review
-- Security critical authentication/authorization code
-- Code you don’t fully understand
-- Large architectural changes
-- Database migrations or schema changes
-
-**Workflow Tips**
-
-- Start small and validate often—build, lint, and test incrementally
-- Study existing patterns before generating new code
-- Always ask: “Is this secure? Does it follow project patterns? What edge cases need testing?”
-
-**Security Considerations**
-
-- Extra review required for MCP servers, network code, file system ops, user input, and credential handling
-- Never expose secrets in prompts
-- Sanitize inputs/outputs and follow goose’s security patterns
-
----
-
-## Testing & Review
-
-Before submitting AI assisted code, confirm that:
-- You understand every line
-- All tests pass locally (happy path + error cases)
-- Docs are updated and accurate
-- Code follows existing patterns
-
-**Always get human review** for:
-
-- Security sensitive code
-- Core architecture changes
-- Async/concurrency logic
-- MCP protocol implementations
-- Large refactors or anything you’re unsure about
-
----
-
-## Using goose for goose Development
-
-- Protect sensitive files with `.gooseignore` (e.g., `.env*`, `*.key`, `target/`, `.git/`)
-- Guide Goose with `.goosehints` (patterns, error handling, formatting, tests, docs)
-- Use `/plan` to structure work, and choose modes wisely:
- - **Chat** for understanding
- - **Smart Approval** for most dev work
- - **Approval** for critical areas
- - **Autonomous** only with safety nets
-
----
-
-## Community & Collaboration
-
-- In PRs, note significant AI use and how you validated results
-- Share prompting tips, patterns, and pitfalls
-- Be responsive to feedback and help improve this guide
-
----
-
-## Remember
-
-AI is a powerful assistant, not a replacement for your judgment. Use it to speed up development; while keeping your brain engaged, your standards high, and goose secure.
-
-Questions? Join our [Discord](https://discord.gg/goose-oss) or [GitHub Discussions](https://github.com/block/goose/discussions) to talk more about responsible AI development.
diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs
index e735f2b82b..85d83435cc 100644
--- a/crates/goose-cli/src/cli.rs
+++ b/crates/goose-cli/src/cli.rs
@@ -19,6 +19,7 @@ use crate::commands::session::{handle_session_list, handle_session_remove};
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml};
use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
+use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose_bench::bench_config::BenchRunConfig;
use goose_bench::runners::bench_runner::BenchRunner;
@@ -86,9 +87,12 @@ async fn get_or_create_session_id(
.ok_or_else(|| anyhow::anyhow!("No session found to resume"))?;
Ok(Some(session_id))
} else {
- let session =
- SessionManager::create_session(std::env::current_dir()?, "CLI Session".to_string())
- .await?;
+ let session = SessionManager::create_session(
+ std::env::current_dir()?,
+ "CLI Session".to_string(),
+ SessionType::User,
+ )
+ .await?;
Ok(Some(session.id))
};
};
@@ -105,8 +109,12 @@ async fn get_or_create_session_id(
.ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))?;
Ok(Some(session_id))
} else {
- let session =
- SessionManager::create_session(std::env::current_dir()?, name.clone()).await?;
+ let session = SessionManager::create_session(
+ std::env::current_dir()?,
+ name.clone(),
+ SessionType::User,
+ )
+ .await?;
SessionManager::update_session(&session.id)
.user_provided_name(name)
@@ -123,9 +131,12 @@ async fn get_or_create_session_id(
.ok_or_else(|| anyhow::anyhow!("Could not extract session ID from path: {:?}", path))?;
Ok(Some(session_id))
} else {
- let session =
- SessionManager::create_session(std::env::current_dir()?, "CLI Session".to_string())
- .await?;
+ let session = SessionManager::create_session(
+ std::env::current_dir()?,
+ "CLI Session".to_string(),
+ SessionType::User,
+ )
+ .await?;
Ok(Some(session.id))
}
}
@@ -773,6 +784,16 @@ enum Command {
)]
additional_sub_recipes: Vec,
+ /// Output format (text, json)
+ #[arg(
+ long = "output-format",
+ value_name = "FORMAT",
+ help = "Output format (text, json)",
+ default_value = "text",
+ value_parser = clap::builder::PossibleValuesParser::new(["text", "json"])
+ )]
+ output_format: String,
+
/// Provider to use for this run (overrides environment variable)
#[arg(
long = "provider",
@@ -1051,6 +1072,7 @@ pub async fn cli() -> anyhow::Result<()> {
sub_recipes: None,
final_output_response: None,
retry_config: None,
+ output_format: "text".to_string(),
})
.await;
@@ -1065,7 +1087,7 @@ pub async fn cli() -> anyhow::Result<()> {
let exit_type = if result.is_ok() { "normal" } else { "error" };
let (total_tokens, message_count) = session
- .get_metadata()
+ .get_session()
.await
.map(|m| (m.total_tokens.unwrap_or(0), m.message_count))
.unwrap_or((0, 0));
@@ -1131,6 +1153,7 @@ pub async fn cli() -> anyhow::Result<()> {
scheduled_job_id,
quiet,
additional_sub_recipes,
+ output_format,
provider,
model,
}) => {
@@ -1260,6 +1283,7 @@ pub async fn cli() -> anyhow::Result<()> {
.as_ref()
.and_then(|r| r.final_output_response.clone()),
retry_config: recipe_info.as_ref().and_then(|r| r.retry_config.clone()),
+ output_format,
})
.await;
@@ -1286,7 +1310,7 @@ pub async fn cli() -> anyhow::Result<()> {
let exit_type = if result.is_ok() { "normal" } else { "error" };
let (total_tokens, message_count) = session
- .get_metadata()
+ .get_session()
.await
.map(|m| (m.total_tokens.unwrap_or(0), m.message_count))
.unwrap_or((0, 0));
@@ -1443,6 +1467,7 @@ pub async fn cli() -> anyhow::Result<()> {
sub_recipes: None,
final_output_response: None,
retry_config: None,
+ output_format: "text".to_string(),
})
.await;
session.interactive(None).await?;
diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs
index 8ee9acfe09..dfdbe269b8 100644
--- a/crates/goose-cli/src/commands/acp.rs
+++ b/crates/goose-cli/src/commands/acp.rs
@@ -3,11 +3,13 @@ use agent_client_protocol::{
ToolCallContent,
};
use anyhow::Result;
-use goose::agents::Agent;
+use goose::agents::{Agent, SessionConfig};
use goose::config::{get_all_extensions, Config};
use goose::conversation::message::{Message, MessageContent};
use goose::conversation::Conversation;
use goose::providers::create;
+use goose::session::session_manager::SessionType;
+use goose::session::SessionManager;
use rmcp::model::{RawContent, ResourceContents};
use std::collections::{HashMap, HashSet};
use std::fs;
@@ -19,17 +21,15 @@ use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use url::Url;
-/// Represents a single goose session for ACP
-struct GooseSession {
+struct GooseAcpSession {
messages: Conversation,
tool_call_ids: HashMap, // Maps internal tool IDs to ACP tool call IDs
cancel_token: Option, // Active cancellation token for prompt processing
}
-/// goose ACP Agent implementation that connects to real goose agents
struct GooseAcpAgent {
- session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>,
- sessions: Arc>>,
+ session_update_tx: mpsc::UnboundedSender<(SessionNotification, oneshot::Sender<()>)>,
+ sessions: Arc>>,
agent: Agent, // Shared agent instance
}
@@ -97,15 +97,14 @@ impl GooseAcpAgent {
async fn new(
session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>,
) -> Result {
- // Load config and create provider
let config = Config::global();
let provider_name: String = config
- .get_param("GOOSE_PROVIDER")
+ .get_goose_provider()
.map_err(|e| anyhow::anyhow!("No provider configured: {}", e))?;
let model_name: String = config
- .get_param("GOOSE_MODEL")
+ .get_goose_model()
.map_err(|e| anyhow::anyhow!("No model configured: {}", e))?;
let model_config = goose::model::ModelConfig {
@@ -217,7 +216,7 @@ impl GooseAcpAgent {
&self,
content_item: &MessageContent,
session_id: &acp::SessionId,
- session: &mut GooseSession,
+ session: &mut GooseAcpSession,
) -> Result<(), acp::Error> {
match content_item {
MessageContent::Text(text) => {
@@ -273,7 +272,7 @@ impl GooseAcpAgent {
&self,
tool_request: &goose::conversation::message::ToolRequest,
session_id: &acp::SessionId,
- session: &mut GooseSession,
+ session: &mut GooseAcpSession,
) -> Result<(), acp::Error> {
// Generate ACP tool call ID and track mapping
let acp_tool_id = format!("tool_{}", uuid::Uuid::new_v4());
@@ -341,7 +340,7 @@ impl GooseAcpAgent {
&self,
tool_response: &goose::conversation::message::ToolResponse,
session_id: &acp::SessionId,
- session: &mut GooseSession,
+ session: &mut GooseAcpSession,
) -> Result<(), acp::Error> {
// Look up the ACP tool call ID
if let Some(acp_tool_id) = session.tool_call_ids.get(&tool_response.id) {
@@ -496,7 +495,7 @@ impl acp::Agent for GooseAcpAgent {
// Generate a unique session ID
let session_id = uuid::Uuid::new_v4().to_string();
- let session = GooseSession {
+ let session = GooseAcpSession {
messages: Conversation::new_unvalidated(Vec::new()),
tool_call_ids: HashMap::new(),
cancel_token: None,
@@ -544,30 +543,26 @@ impl acp::Agent for GooseAcpAgent {
// Create and store cancellation token for this prompt
let cancel_token = CancellationToken::new();
- // Convert ACP prompt to Goose message
let user_message = self.convert_acp_prompt_to_message(args.prompt);
- // Prepare for agent reply
- let messages = {
- let mut sessions = self.sessions.lock().await;
- let session = sessions
- .get_mut(&session_id)
- .ok_or_else(acp::Error::invalid_params)?;
+ let session = SessionManager::create_session(
+ std::env::current_dir().unwrap_or_default(),
+ "ACP Session".to_string(),
+ SessionType::Hidden,
+ )
+ .await?;
- // Add message to conversation
- session.messages.push(user_message);
-
- // Store cancellation token
- session.cancel_token = Some(cancel_token.clone());
-
- // Clone what we need for the reply call
- session.messages.clone()
+ let session_config = SessionConfig {
+ id: session.id.clone(),
+ schedule_id: None,
+ max_turns: None,
+ retry_config: None,
};
// Get agent's reply through the Goose agent
let mut stream = self
.agent
- .reply(messages, None, Some(cancel_token.clone()))
+ .reply(user_message, session_config, Some(cancel_token.clone()))
.await
.map_err(|e| {
error!("Error getting agent reply: {}", e);
diff --git a/crates/goose-cli/src/commands/bench.rs b/crates/goose-cli/src/commands/bench.rs
index d67b919131..c0005fa540 100644
--- a/crates/goose-cli/src/commands/bench.rs
+++ b/crates/goose-cli/src/commands/bench.rs
@@ -26,9 +26,7 @@ impl BenchBaseSession for CliSession {
}
fn get_session_id(&self) -> anyhow::Result {
- self.session_id()
- .cloned()
- .ok_or_else(|| anyhow::anyhow!("No session ID available"))
+ Ok(self.session_id().to_string())
}
}
pub async fn agent_generator(
@@ -57,6 +55,7 @@ pub async fn agent_generator(
sub_recipes: None,
final_output_response: None,
retry_config: None,
+ output_format: "text".to_string(),
})
.await;
diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs
index bf0ef6a0e6..82653c08e9 100644
--- a/crates/goose-cli/src/commands/configure.rs
+++ b/crates/goose-cli/src/commands/configure.rs
@@ -14,7 +14,8 @@ use goose::config::paths::Paths;
use goose::config::permission::PermissionLevel;
use goose::config::signup_tetrate::TetrateAuth;
use goose::config::{
- configure_tetrate, Config, ConfigError, ExperimentManager, ExtensionEntry, PermissionManager,
+ configure_tetrate, Config, ConfigError, ExperimentManager, ExtensionEntry, GooseMode,
+ PermissionManager,
};
use goose::conversation::message::Message;
use goose::model::ModelConfig;
@@ -421,7 +422,7 @@ fn select_model_from_list(
}
fn try_store_secret(config: &Config, key_name: &str, value: String) -> anyhow::Result {
- match config.set_secret(key_name, Value::String(value)) {
+ match config.set_secret(key_name, &value) {
Ok(_) => Ok(true),
Err(e) => {
cliclack::outro(style(format!(
@@ -450,7 +451,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result {
.collect();
// Get current default provider if it exists
- let current_provider: Option = config.get_param("GOOSE_PROVIDER").ok();
+ let current_provider: Option = config.get_goose_provider().ok();
let default_provider = current_provider.unwrap_or_default();
// Select provider
@@ -487,7 +488,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result {
return Ok(false);
}
} else {
- config.set_param(&key.name, Value::String(env_value))?;
+ config.set_param(&key.name, &env_value)?;
}
let _ = cliclack::log::info(format!("Saved {} to {}", key.name, config.path()));
}
@@ -529,7 +530,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result {
return Ok(false);
}
} else {
- config.set_param(&key.name, Value::String(value))?;
+ config.set_param(&key.name, &value)?;
}
}
}
@@ -558,9 +559,9 @@ pub async fn configure_provider_dialog() -> anyhow::Result {
};
if key.secret {
- config.set_secret(&key.name, Value::String(value))?;
+ config.set_secret(&key.name, &value)?;
} else {
- config.set_param(&key.name, Value::String(value))?;
+ config.set_param(&key.name, &value)?;
}
}
}
@@ -648,9 +649,8 @@ pub async fn configure_provider_dialog() -> anyhow::Result {
match result {
Ok((_message, _usage)) => {
- // Update config with new values only if the test succeeds
- config.set_param("GOOSE_PROVIDER", Value::String(provider_name.to_string()))?;
- config.set_param("GOOSE_MODEL", Value::String(model.clone()))?;
+ config.set_goose_provider(provider_name)?;
+ config.set_goose_model(&model)?;
print_config_file_saved()?;
Ok(true)
}
@@ -877,7 +877,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> {
// Try to store in keychain
let keychain_key = key.to_string();
- match config.set_secret(&keychain_key, Value::String(value.clone())) {
+ match config.set_secret(&keychain_key, &value) {
Ok(_) => {
// Successfully stored in keychain, add to env_keys
env_keys.push(keychain_key);
@@ -973,7 +973,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> {
// Try to store in keychain
let keychain_key = key.to_string();
- match config.set_secret(&keychain_key, Value::String(value.clone())) {
+ match config.set_secret(&keychain_key, &value) {
Ok(_) => {
// Successfully stored in keychain, add to env_keys
env_keys.push(keychain_key);
@@ -1093,7 +1093,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> {
// Try to store in keychain
let keychain_key = key.to_string();
- match config.set_secret(&keychain_key, Value::String(value.clone())) {
+ match config.set_secret(&keychain_key, &Value::String(value.clone())) {
Ok(_) => {
// Successfully stored in keychain, add to env_keys
env_keys.push(keychain_key);
@@ -1273,46 +1273,35 @@ pub fn configure_goose_mode_dialog() -> anyhow::Result<()> {
let mode = cliclack::select("Which goose mode would you like to configure?")
.item(
- "auto",
+ GooseMode::Auto,
"Auto Mode",
"Full file modification, extension usage, edit, create and delete files freely"
)
.item(
- "approve",
+ GooseMode::Approve,
"Approve Mode",
"All tools, extensions and file modifications will require human approval"
)
.item(
- "smart_approve",
+ GooseMode::SmartApprove,
"Smart Approve Mode",
"Editing, creating, deleting files and using extensions will require human approval"
)
.item(
- "chat",
+ GooseMode::Chat,
"Chat Mode",
"Engage with the selected provider without using tools, extensions, or file modification"
)
.interact()?;
- match mode {
- "auto" => {
- config.set_param("GOOSE_MODE", Value::String("auto".to_string()))?;
- cliclack::outro("Set to Auto Mode - full file modification enabled")?;
- }
- "approve" => {
- config.set_param("GOOSE_MODE", Value::String("approve".to_string()))?;
- cliclack::outro("Set to Approve Mode - all tools and modifications require approval")?;
- }
- "smart_approve" => {
- config.set_param("GOOSE_MODE", Value::String("smart_approve".to_string()))?;
- cliclack::outro("Set to Smart Approve Mode - modifications require approval")?;
- }
- "chat" => {
- config.set_param("GOOSE_MODE", Value::String("chat".to_string()))?;
- cliclack::outro("Set to Chat Mode - no tools or modifications enabled")?;
- }
- _ => unreachable!(),
+ config.set_goose_mode(mode)?;
+ let msg = match mode {
+ GooseMode::Auto => "Set to Auto Mode - full file modification enabled",
+ GooseMode::Approve => "Set to Approve Mode - all tools and modifications require approval",
+ GooseMode::SmartApprove => "Set to Smart Approve Mode - modifications require approval",
+ GooseMode::Chat => "Set to Chat Mode - no tools or modifications enabled",
};
+ cliclack::outro(msg)?;
Ok(())
}
@@ -1321,28 +1310,25 @@ pub fn configure_goose_router_strategy_dialog() -> anyhow::Result<()> {
let enable_router = cliclack::select("Would you like to enable smart tool routing?")
.item(
- "true",
+ true,
"Enable Router",
"Use LLM-based intelligence to select tools",
)
.item(
- "false",
+ false,
"Disable Router",
"Use the default tool selection strategy",
)
.interact()?;
- match enable_router {
- "true" => {
- config.set_param("GOOSE_ENABLE_ROUTER", Value::String("true".to_string()))?;
- cliclack::outro("Router enabled - using LLM-based intelligence for tool selection")?;
- }
- "false" => {
- config.set_param("GOOSE_ENABLE_ROUTER", Value::String("false".to_string()))?;
- cliclack::outro("Router disabled - using default tool selection")?;
- }
- _ => unreachable!(),
+ config.set_param("GOOSE_ENABLE_ROUTER", enable_router)?;
+ let msg = if enable_router {
+ "Router enabled - using LLM-based intelligence for tool selection"
+ } else {
+ "Router disabled - using default tool selection"
};
+ cliclack::outro(msg)?;
+
Ok(())
}
@@ -1360,15 +1346,15 @@ pub fn configure_tool_output_dialog() -> anyhow::Result<()> {
match tool_log_level {
"high" => {
- config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.8))?;
+ config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.8)?;
cliclack::outro("Showing tool output of high importance only.")?;
}
"medium" => {
- config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.2))?;
+ config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.2)?;
cliclack::outro("Showing tool output of medium importance.")?;
}
"all" => {
- config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.0))?;
+ config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.0)?;
cliclack::outro("Showing all tool output.")?;
}
_ => unreachable!(),
@@ -1441,11 +1427,11 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
let config = Config::global();
let provider_name: String = config
- .get_param("GOOSE_PROVIDER")
+ .get_goose_provider()
.expect("No provider configured. Please set model provider first");
let model: String = config
- .get_param("GOOSE_MODEL")
+ .get_goose_model()
.expect("No model configured. Please set model first");
let model_config = ModelConfig::new(&model)?;
@@ -1591,7 +1577,7 @@ fn configure_recipe_dialog() -> anyhow::Result<()> {
if input_value.clone().trim().is_empty() {
config.delete(key_name)?;
} else {
- config.set_param(key_name, Value::String(input_value))?;
+ config.set_param(key_name, &input_value)?;
}
Ok(())
}
@@ -1618,7 +1604,7 @@ pub fn configure_max_turns_dialog() -> anyhow::Result<()> {
.interact()?;
let max_turns: u32 = max_turns_input.parse()?;
- config.set_param("GOOSE_MAX_TURNS", Value::from(max_turns))?;
+ config.set_param("GOOSE_MAX_TURNS", max_turns)?;
cliclack::outro(format!(
"Set maximum turns to {} - goose will ask for input after {} consecutive actions",
@@ -1651,7 +1637,7 @@ pub async fn handle_openrouter_auth() -> anyhow::Result<()> {
// Test configuration - get the model that was configured
println!("\nTesting configuration...");
- let configured_model: String = config.get_param("GOOSE_MODEL")?;
+ let configured_model: String = config.get_goose_model()?;
let model_config = match goose::model::ModelConfig::new(&configured_model) {
Ok(config) => config,
Err(e) => {
@@ -1729,7 +1715,7 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> {
// Test configuration
println!("\nTesting configuration...");
- let configured_model: String = config.get_param("GOOSE_MODEL")?;
+ let configured_model: String = config.get_goose_model()?;
let model_config = match goose::model::ModelConfig::new(&configured_model) {
Ok(config) => config,
Err(e) => {
diff --git a/crates/goose-cli/src/commands/info.rs b/crates/goose-cli/src/commands/info.rs
index 0f0debc1c2..de3b838314 100644
--- a/crates/goose-cli/src/commands/info.rs
+++ b/crates/goose-cli/src/commands/info.rs
@@ -11,6 +11,7 @@ fn print_aligned(label: &str, value: &str, width: usize) {
pub fn handle_info(verbose: bool) -> Result<()> {
let logs_dir = Paths::in_state_dir("logs");
let sessions_dir = Paths::in_data_dir("sessions");
+ let sessions_db = sessions_dir.join("sessions.db");
// Get paths using a stored reference to the global config
let config = Config::global();
@@ -19,7 +20,7 @@ pub fn handle_info(verbose: bool) -> Result<()> {
// Define the labels and their corresponding path values once.
let paths = [
("Config dir:", config_dir),
- ("Sessions dir:", sessions_dir.display().to_string()),
+ ("Sessions DB (sqlite):", sessions_db.display().to_string()),
("Logs dir:", logs_dir.display().to_string()),
];
@@ -40,26 +41,22 @@ pub fn handle_info(verbose: bool) -> Result<()> {
// Print verbose info if requested
if verbose {
println!("\n{}", style("goose Configuration:").cyan().bold());
- match config.load_values() {
- Ok(values) => {
- if values.is_empty() {
- println!(" No configuration values set");
- println!(
- " Run '{}' to configure goose",
- style("goose configure").cyan()
- );
- } else {
- let sorted_values: std::collections::BTreeMap<_, _> =
- values.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
+ let values = config.all_values()?;
+ if values.is_empty() {
+ println!(" No configuration values set");
+ println!(
+ " Run '{}' to configure goose",
+ style("goose configure").cyan()
+ );
+ } else {
+ let sorted_values: std::collections::BTreeMap<_, _> =
+ values.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
- if let Ok(yaml) = serde_yaml::to_string(&sorted_values) {
- for line in yaml.lines() {
- println!(" {}", line);
- }
- }
+ if let Ok(yaml) = serde_yaml::to_string(&sorted_values) {
+ for line in yaml.lines() {
+ println!(" {}", line);
}
}
- Err(e) => println!(" Error loading configuration: {}", e),
}
}
diff --git a/crates/goose-cli/src/commands/schedule.rs b/crates/goose-cli/src/commands/schedule.rs
index 2756fa2e44..12f2c08684 100644
--- a/crates/goose-cli/src/commands/schedule.rs
+++ b/crates/goose-cli/src/commands/schedule.rs
@@ -98,7 +98,6 @@ pub async fn handle_schedule_add(
paused: false,
current_session_id: None,
process_start_time: None,
- execution_mode: Some("background".to_string()), // Default to background for CLI
};
let scheduler_storage_path =
diff --git a/crates/goose-cli/src/commands/web.rs b/crates/goose-cli/src/commands/web.rs
index 636b2590eb..8fa49a6645 100644
--- a/crates/goose-cli/src/commands/web.rs
+++ b/crates/goose-cli/src/commands/web.rs
@@ -15,6 +15,7 @@ use base64::Engine;
use futures::{sink::SinkExt, stream::StreamExt};
use goose::agents::{Agent, AgentEvent};
use goose::conversation::message::Message as GooseMessage;
+use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -139,7 +140,7 @@ pub async fn handle_web(
let config = goose::config::Config::global();
- let provider_name: String = match config.get_param("GOOSE_PROVIDER") {
+ let provider_name: String = match config.get_goose_provider() {
Ok(p) => p,
Err(_) => {
eprintln!("No provider configured. Run 'goose configure' first");
@@ -147,7 +148,7 @@ pub async fn handle_web(
}
};
- let model: String = match config.get_param("GOOSE_MODEL") {
+ let model: String = match config.get_goose_model() {
Ok(m) => m,
Err(_) => {
eprintln!("No model configured. Run 'goose configure' first");
@@ -226,6 +227,7 @@ async fn serve_index() -> Result {
let session = SessionManager::create_session(
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
"Web session".to_string(),
+ SessionType::User,
)
.await
.map_err(|err| (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
@@ -467,21 +469,16 @@ async fn process_message_streaming(
let session = SessionManager::get_session(&session_id, true).await?;
let mut messages = session.conversation.unwrap_or_default();
- messages.push(user_message);
+ messages.push(user_message.clone());
let session_config = SessionConfig {
id: session.id.clone(),
- working_dir: session.working_dir,
schedule_id: None,
- execution_mode: None,
max_turns: None,
retry_config: None,
};
- match agent
- .reply(messages.clone(), Some(session_config), None)
- .await
- {
+ match agent.reply(user_message, session_config, None).await {
Ok(mut stream) => {
while let Some(result) = stream.next().await {
match result {
diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs
index f65fe5a4df..acd9d16d23 100644
--- a/crates/goose-cli/src/logging.rs
+++ b/crates/goose-cli/src/logging.rs
@@ -1,5 +1,4 @@
use anyhow::{Context, Result};
-use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Once;
use tokio::sync::Mutex;
@@ -16,12 +15,6 @@ use goose_bench::error_capture::ErrorCaptureLayer;
// Used to ensure we only set up tracing once
static INIT: Once = Once::new();
-/// Returns the directory where log files should be stored.
-/// Creates the directory structure if it doesn't exist.
-fn get_log_directory() -> Result {
- goose::logging::get_log_directory("cli", true)
-}
-
/// Sets up the logging infrastructure for the application.
/// This includes:
/// - File-based logging with JSON formatting (DEBUG level)
@@ -50,20 +43,15 @@ fn setup_logging_internal(
let mut setup = || {
result = (|| {
- // Set up file appender for goose module logs
- let log_dir = get_log_directory()?;
+ let log_dir = goose::logging::prepare_log_directory("cli", true)?;
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
-
- // Create log file name by prefixing with timestamp
let log_filename = if name.is_some() {
format!("{}-{}.log", timestamp, name.unwrap())
} else {
format!("{}.log", timestamp)
};
-
- // Create non-rolling file appender for detailed logs
let file_appender = tracing_appender::rolling::RollingFileAppender::new(
- Rotation::NEVER,
+ Rotation::NEVER, // we do manual rotation via file naming and cleanup_old_logs
log_dir,
log_filename,
);
@@ -177,7 +165,7 @@ mod tests {
#[test]
fn test_log_directory_creation() {
let _temp_dir = setup_temp_home();
- let log_dir = get_log_directory().unwrap();
+ let log_dir = goose::logging::prepare_log_directory("cli", true).unwrap();
assert!(log_dir.exists());
assert!(log_dir.is_dir());
diff --git a/crates/goose-cli/src/recipes/recipe.rs b/crates/goose-cli/src/recipes/recipe.rs
index 987b7d295b..ff72265728 100644
--- a/crates/goose-cli/src/recipes/recipe.rs
+++ b/crates/goose-cli/src/recipes/recipe.rs
@@ -11,7 +11,6 @@ use goose::recipe::build_recipe::{
};
use goose::recipe::validate_recipe::parse_and_validate_parameters;
use goose::recipe::Recipe;
-use serde_json::Value;
fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result {
|key: &str, description: &str| -> Result {
@@ -98,7 +97,7 @@ pub fn collect_missing_secrets(requirements: &[SecretRequirement]) -> Result<()>
.unwrap_or_else(|_| String::new());
if !value.trim().is_empty() {
- config.set_secret(&req.key, Value::String(value))?;
+ config.set_secret(&req.key, &value)?;
println!("✅ Secret stored securely for {}", req.extension_name);
} else {
println!("⏭️ Skipped {} for {}", req.key, req.extension_name);
diff --git a/crates/goose-cli/src/scenario_tests/scenario_runner.rs b/crates/goose-cli/src/scenario_tests/scenario_runner.rs
index 00a90778d3..8d7474d485 100644
--- a/crates/goose-cli/src/scenario_tests/scenario_runner.rs
+++ b/crates/goose-cli/src/scenario_tests/scenario_runner.rs
@@ -9,8 +9,10 @@ use anyhow::Result;
use goose::agents::Agent;
use goose::model::ModelConfig;
use goose::providers::{create, testprovider::TestProvider};
+use goose::session::session_manager::SessionType;
+use goose::session::SessionManager;
use std::collections::{HashMap, HashSet};
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
@@ -190,7 +192,6 @@ where
)
};
- // Generate messages using the provider
let messages = vec![message_generator(&*provider_arc)];
let mock_client = weather_client();
@@ -218,11 +219,27 @@ where
.update_provider(provider_arc as Arc)
.await?;
- let mut session = CliSession::new(agent, None, false, None, None, None, None).await;
+ let session = SessionManager::create_session(
+ PathBuf::default(),
+ "scenario-runner".to_string(),
+ SessionType::Hidden,
+ )
+ .await?;
+ let mut cli_session = CliSession::new(
+ agent,
+ session.id,
+ false,
+ None,
+ None,
+ None,
+ None,
+ "text".to_string(),
+ )
+ .await;
let mut error = None;
for message in &messages {
- if let Err(e) = session
+ if let Err(e) = cli_session
.process_message(message.clone(), CancellationToken::default())
.await
{
@@ -230,7 +247,7 @@ where
break;
}
}
- let updated_messages = session.message_history();
+ let updated_messages = cli_session.message_history();
if let Some(ref err_msg) = error {
if err_msg.contains("No recorded response found") {
@@ -249,7 +266,7 @@ where
validator(&result)?;
- drop(session);
+ drop(cli_session);
if let Some(provider) = provider_for_saving {
if result.error.is_none() {
diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs
index 7e0f0fce7d..96321f4bce 100644
--- a/crates/goose-cli/src/session/builder.rs
+++ b/crates/goose-cli/src/session/builder.rs
@@ -11,6 +11,7 @@ use goose::providers::create;
use goose::recipe::{Response, SubRecipe};
use goose::agents::extension::PlatformExtensionContext;
+use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{EnabledExtensionsState, ExtensionState};
use rustyline::EditMode;
@@ -23,9 +24,9 @@ use tokio::task::JoinSet;
///
/// This struct contains all the parameters needed to create a new session,
/// including session identification, extension configuration, and debug settings.
-#[derive(Default, Clone, Debug)]
+#[derive(Clone, Debug)]
pub struct SessionBuilderConfig {
- /// Optional session ID for resuming or identifying an existing session
+ /// Session id, optional need to deduce from context
pub session_id: Option,
/// Whether to resume an existing session
pub resume: bool,
@@ -67,6 +68,39 @@ pub struct SessionBuilderConfig {
pub final_output_response: Option,
/// Retry configuration for automated validation and recovery
pub retry_config: Option,
+ /// Output format (text, json)
+ pub output_format: String,
+}
+
+/// Manual implementation of Default to ensure proper initialization of output_format
+/// This struct requires explicit default value for output_format field
+impl Default for SessionBuilderConfig {
+ fn default() -> Self {
+ SessionBuilderConfig {
+ session_id: None,
+ resume: false,
+ no_session: false,
+ extensions: Vec::new(),
+ remote_extensions: Vec::new(),
+ streamable_http_extensions: Vec::new(),
+ builtins: Vec::new(),
+ extensions_override: None,
+ additional_system_prompt: None,
+ settings: None,
+ provider: None,
+ model: None,
+ debug: false,
+ max_tool_repetitions: None,
+ max_turns: None,
+ scheduled_job_id: None,
+ interactive: false,
+ quiet: false,
+ sub_recipes: None,
+ final_output_response: None,
+ retry_config: None,
+ output_format: "text".to_string(),
+ }
+ }
}
/// Offers to help debug an extension failure by creating a minimal debugging session
@@ -132,8 +166,23 @@ async fn offer_extension_debugging_help(
}
}
- // Create the debugging session
- let mut debug_session = CliSession::new(debug_agent, None, false, None, None, None, None).await;
+ let session = SessionManager::create_session(
+ std::env::current_dir()?,
+ "CLI Session".to_string(),
+ SessionType::Hidden,
+ )
+ .await?;
+ let mut debug_session = CliSession::new(
+ debug_agent,
+ session.id,
+ false,
+ None,
+ None,
+ None,
+ None,
+ "text".to_string(),
+ )
+ .await;
// Process the debugging request
println!("{}", style("Analyzing the extension failure...").yellow());
@@ -208,7 +257,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
.as_ref()
.and_then(|s| s.goose_provider.clone())
})
- .or_else(|| config.get_param("GOOSE_PROVIDER").ok())
+ .or_else(|| config.get_goose_provider().ok())
.expect("No provider configured. Run 'goose configure' first");
let model_name = session_config
@@ -219,7 +268,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
.as_ref()
.and_then(|s| s.goose_model.clone())
})
- .or_else(|| config.get_param("GOOSE_MODEL").ok())
+ .or_else(|| config.get_goose_model().ok())
.expect("No model configured. Run 'goose configure' first");
let temperature = session_config.settings.as_ref().and_then(|s| s.temperature);
@@ -278,12 +327,20 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
process::exit(1);
});
- let session_id: Option = if session_config.no_session {
- None
+ let session_id: String = if session_config.no_session {
+ let working_dir = std::env::current_dir().expect("Could not get working directory");
+ let session = SessionManager::create_session(
+ working_dir,
+ "CLI Session".to_string(),
+ SessionType::Hidden,
+ )
+ .await
+ .expect("Could not create session");
+ session.id
} else if session_config.resume {
if let Some(session_id) = session_config.session_id {
match SessionManager::get_session(&session_id, false).await {
- Ok(_) => Some(session_id),
+ Ok(_) => session_id,
Err(_) => {
output::render_error(&format!(
"Cannot resume session {} - no such session exists",
@@ -294,7 +351,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
}
} else {
match SessionManager::list_sessions().await {
- Ok(sessions) if !sessions.is_empty() => Some(sessions[0].id.clone()),
+ Ok(sessions) if !sessions.is_empty() => sessions[0].id.clone(),
_ => {
output::render_error("Cannot resume - no previous sessions found");
process::exit(1);
@@ -302,46 +359,44 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
}
}
} else {
- session_config.session_id
+ session_config.session_id.unwrap()
};
agent
.extension_manager
.set_context(PlatformExtensionContext {
- session_id: session_id.clone(),
+ session_id: Some(session_id.clone()),
extension_manager: Some(Arc::downgrade(&agent.extension_manager)),
tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)),
})
.await;
if session_config.resume {
- if let Some(session_id) = session_id.as_ref() {
- let metadata = SessionManager::get_session(session_id, false)
- .await
- .unwrap_or_else(|e| {
- output::render_error(&format!("Failed to read session metadata: {}", e));
- process::exit(1);
- });
+ let session = SessionManager::get_session(&session_id, false)
+ .await
+ .unwrap_or_else(|e| {
+ output::render_error(&format!("Failed to read session metadata: {}", e));
+ process::exit(1);
+ });
- let current_workdir =
- std::env::current_dir().expect("Failed to get current working directory");
- if current_workdir != metadata.working_dir {
- let change_workdir = cliclack::confirm(format!("{} The original working directory of this session was set to {}. Your current directory is {}. Do you want to switch back to the original working directory?", style("WARNING:").yellow(), style(metadata.working_dir.display()).cyan(), style(current_workdir.display()).cyan()))
+ let current_workdir =
+ std::env::current_dir().expect("Failed to get current working directory");
+ if current_workdir != session.working_dir {
+ let change_workdir = cliclack::confirm(format!("{} The original working directory of this session was set to {}. Your current directory is {}. Do you want to switch back to the original working directory?", style("WARNING:").yellow(), style(session.working_dir.display()).cyan(), style(current_workdir.display()).cyan()))
.initial_value(true)
.interact().expect("Failed to get user input");
- if change_workdir {
- if !metadata.working_dir.exists() {
- output::render_error(&format!(
- "Cannot switch to original working directory - {} no longer exists",
- style(metadata.working_dir.display()).cyan()
- ));
- } else if let Err(e) = std::env::set_current_dir(&metadata.working_dir) {
- output::render_error(&format!(
- "Failed to switch to original working directory: {}",
- e
- ));
- }
+ if change_workdir {
+ if !session.working_dir.exists() {
+ output::render_error(&format!(
+ "Cannot switch to original working directory - {} no longer exists",
+ style(session.working_dir.display()).cyan()
+ ));
+ } else if let Err(e) = std::env::set_current_dir(&session.working_dir) {
+ output::render_error(&format!(
+ "Failed to switch to original working directory: {}",
+ e
+ ));
}
}
}
@@ -354,22 +409,18 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
agent.disable_router_for_recipe().await;
extensions.into_iter().collect()
} else if session_config.resume {
- if let Some(session_id) = session_id.as_ref() {
- match SessionManager::get_session(session_id, false).await {
- Ok(session_data) => {
- if let Some(saved_state) =
- EnabledExtensionsState::from_extension_data(&session_data.extension_data)
- {
- check_missing_extensions_or_exit(&saved_state.extensions);
- saved_state.extensions
- } else {
- get_enabled_extensions()
- }
+ match SessionManager::get_session(&session_id, false).await {
+ Ok(session_data) => {
+ if let Some(saved_state) =
+ EnabledExtensionsState::from_extension_data(&session_data.extension_data)
+ {
+ check_missing_extensions_or_exit(&saved_state.extensions);
+ saved_state.extensions
+ } else {
+ get_enabled_extensions()
}
- _ => get_enabled_extensions(),
}
- } else {
- get_enabled_extensions()
+ _ => get_enabled_extensions(),
}
} else {
get_enabled_extensions()
@@ -450,6 +501,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
session_config.max_turns,
edit_mode,
session_config.retry_config.clone(),
+ session_config.output_format.clone(),
)
.await;
@@ -560,23 +612,19 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
}
}
- if let Some(session_id) = session_id.as_ref() {
- let session_config_for_save = SessionConfig {
- id: session_id.clone(),
- working_dir: std::env::current_dir().unwrap_or_default(),
- schedule_id: None,
- execution_mode: None,
- max_turns: None,
- retry_config: None,
- };
+ let session_config_for_save = SessionConfig {
+ id: session_id.clone(),
+ schedule_id: None,
+ max_turns: None,
+ retry_config: None,
+ };
- if let Err(e) = session
- .agent
- .save_extension_state(&session_config_for_save)
- .await
- {
- tracing::warn!("Failed to save initial extension state: {}", e);
- }
+ if let Err(e) = session
+ .agent
+ .save_extension_state(&session_config_for_save)
+ .await
+ {
+ tracing::warn!("Failed to save initial extension state: {}", e);
}
// Add CLI-specific system prompt extension
@@ -603,7 +651,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
session_config.resume,
&provider_name,
&model_name,
- &session_id,
+ &Some(session_id),
Some(&provider_for_display),
);
}
@@ -638,6 +686,7 @@ mod tests {
sub_recipes: None,
final_output_response: None,
retry_config: None,
+ output_format: "text".to_string(),
};
assert_eq!(config.extensions.len(), 1);
diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs
index 6702104745..6c32529a9a 100644
--- a/crates/goose-cli/src/session/completion.rs
+++ b/crates/goose-cli/src/session/completion.rs
@@ -26,7 +26,7 @@ impl GooseCompleter {
/// Complete prompt names for the /prompt command
fn complete_prompt_names(&self, line: &str) -> Result<(usize, Vec)> {
// Get the prefix of the prompt name being typed
- let prefix = if line.len() > 8 { &line[8..] } else { "" };
+ let prefix = line.get(8..).unwrap_or("");
// Get available prompts from cache
let cache = self.completion_cache.read().unwrap();
@@ -156,7 +156,7 @@ impl GooseCompleter {
/// Complete argument keys for a specific prompt
fn complete_argument_keys(&self, line: &str) -> Result<(usize, Vec)> {
- let parts: Vec<&str> = line[8..].split_whitespace().collect();
+ let parts: Vec<&str> = line.get(8..).unwrap_or("").split_whitespace().collect();
// We need at least the prompt name
if parts.is_empty() {
diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs
index 987415d125..a1d44b877f 100644
--- a/crates/goose-cli/src/session/input.rs
+++ b/crates/goose-cli/src/session/input.rs
@@ -20,7 +20,7 @@ pub enum InputResult {
EndPlan,
Clear,
Recipe(Option),
- Summarize,
+ Compact,
}
#[derive(Debug)]
@@ -120,7 +120,8 @@ fn handle_slash_command(input: &str) -> Option {
const CMD_ENDPLAN: &str = "/endplan";
const CMD_CLEAR: &str = "/clear";
const CMD_RECIPE: &str = "/recipe";
- const CMD_SUMMARIZE: &str = "/summarize";
+ const CMD_COMPACT: &str = "/compact";
+ const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize";
match input {
"/exit" | "/quit" => Some(InputResult::Exit),
@@ -168,19 +169,25 @@ fn handle_slash_command(input: &str) -> Option {
}
}
s if s.starts_with(CMD_EXTENSION) => Some(InputResult::AddExtension(
- s[CMD_EXTENSION.len()..].to_string(),
+ s.get(CMD_EXTENSION.len()..).unwrap_or("").to_string(),
)),
- s if s.starts_with(CMD_BUILTIN) => {
- Some(InputResult::AddBuiltin(s[CMD_BUILTIN.len()..].to_string()))
+ s if s.starts_with(CMD_BUILTIN) => Some(InputResult::AddBuiltin(
+ s.get(CMD_BUILTIN.len()..).unwrap_or("").to_string(),
+ )),
+ s if s.starts_with(CMD_MODE) => Some(InputResult::GooseMode(
+ s.get(CMD_MODE.len()..).unwrap_or("").to_string(),
+ )),
+ s if s.starts_with(CMD_PLAN) => {
+ parse_plan_command(s.get(CMD_PLAN.len()..).unwrap_or("").trim().to_string())
}
- s if s.starts_with(CMD_MODE) => {
- Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string()))
- }
- s if s.starts_with(CMD_PLAN) => parse_plan_command(s[CMD_PLAN.len()..].trim().to_string()),
s if s == CMD_ENDPLAN => Some(InputResult::EndPlan),
s if s == CMD_CLEAR => Some(InputResult::Clear),
s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s),
- s if s == CMD_SUMMARIZE => Some(InputResult::Summarize),
+ s if s == CMD_COMPACT => Some(InputResult::Compact),
+ s if s == CMD_SUMMARIZE_DEPRECATED => {
+ println!("{}", console::style("⚠️ Note: /summarize has been renamed to /compact and will be removed in a future release.").yellow());
+ Some(InputResult::Compact)
+ }
_ => None,
}
}
@@ -194,7 +201,7 @@ fn parse_recipe_command(s: &str) -> Option {
}
// Extract the filepath from the command
- let filepath = s[CMD_RECIPE.len()..].trim();
+ let filepath = s.get(CMD_RECIPE.len()..).unwrap_or("").trim();
if filepath.is_empty() {
return Some(InputResult::Recipe(None));
@@ -305,7 +312,7 @@ fn print_help() {
/endplan - Exit plan mode and return to 'normal' goose mode.
/recipe [filepath] - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml).
If no filepath is provided, it will be saved to ./recipe.yaml.
-/summarize - Summarize the current conversation to reduce context length while preserving key information.
+/compact - Compact the current conversation to reduce context length while preserving key information.
/? or /help - Display this help message
/clear - Clears the current chat history
@@ -541,17 +548,6 @@ mod tests {
assert!(matches!(result, Some(InputResult::Retry)));
}
- #[test]
- fn test_summarize_command() {
- // Test the summarize command
- let result = handle_slash_command("/summarize");
- assert!(matches!(result, Some(InputResult::Summarize)));
-
- // Test with whitespace
- let result = handle_slash_command(" /summarize ");
- assert!(matches!(result, Some(InputResult::Summarize)));
- }
-
#[test]
fn test_get_input_prompt_string() {
let prompt = get_input_prompt_string();
diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs
index f4712ecb33..35f8999d01 100644
--- a/crates/goose-cli/src/session/mod.rs
+++ b/crates/goose-cli/src/session/mod.rs
@@ -12,6 +12,7 @@ use crate::session::task_execution_display::{
};
use goose::conversation::Conversation;
use std::io::Write;
+use std::str::FromStr;
pub use self::export::message_to_markdown;
pub use builder::{build_session, SessionBuilderConfig, SessionSettings};
@@ -27,8 +28,8 @@ use anyhow::{Context, Result};
use completion::GooseCompleter;
use goose::agents::extension::{Envs, ExtensionConfig};
use goose::agents::types::RetryConfig;
-use goose::agents::{Agent, SessionConfig};
-use goose::config::Config;
+use goose::agents::{Agent, SessionConfig, MANUAL_COMPACT_TRIGGER};
+use goose::config::{Config, GooseMode};
use goose::providers::pricing::initialize_pricing_cache;
use goose::session::SessionManager;
use input::InputResult;
@@ -40,6 +41,7 @@ use goose::config::paths::Paths;
use goose::conversation::message::{Message, MessageContent};
use rand::{distributions::Alphanumeric, Rng};
use rustyline::EditMode;
+use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
@@ -49,6 +51,18 @@ use tokio;
use tokio_util::sync::CancellationToken;
use tracing::warn;
+#[derive(Serialize, Deserialize, Debug)]
+struct JsonOutput {
+ messages: Vec,
+ metadata: JsonMetadata,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct JsonMetadata {
+ total_tokens: Option,
+ status: String,
+}
+
pub enum RunMode {
Normal,
Plan,
@@ -57,7 +71,7 @@ pub enum RunMode {
pub struct CliSession {
agent: Agent,
messages: Conversation,
- session_id: Option,
+ session_id: String,
completion_cache: Arc>,
debug: bool,
run_mode: RunMode,
@@ -65,6 +79,7 @@ pub struct CliSession {
max_turns: Option,
edit_mode: Option,
retry_config: Option,
+ output_format: String,
}
// Cache structure for completion data
@@ -119,23 +134,21 @@ pub async fn classify_planner_response(
}
impl CliSession {
+ #[allow(clippy::too_many_arguments)]
pub async fn new(
agent: Agent,
- session_id: Option,
+ session_id: String,
debug: bool,
scheduled_job_id: Option,
max_turns: Option,
edit_mode: Option,
retry_config: Option,
+ output_format: String,
) -> Self {
- let messages = if let Some(session_id) = &session_id {
- SessionManager::get_session(session_id, true)
- .await
- .map(|session| session.conversation.unwrap_or_default())
- .unwrap()
- } else {
- Conversation::new_unvalidated(Vec::new())
- };
+ let messages = SessionManager::get_session(&session_id, true)
+ .await
+ .map(|session| session.conversation.unwrap_or_default())
+ .unwrap();
CliSession {
agent,
@@ -148,11 +161,12 @@ impl CliSession {
max_turns,
edit_mode,
retry_config,
+ output_format,
}
}
- pub fn session_id(&self) -> Option<&String> {
- self.session_id.as_ref()
+ pub fn session_id(&self) -> &String {
+ &self.session_id
}
/// Add a stdio extension to the session
@@ -358,9 +372,6 @@ impl CliSession {
cancel_token: CancellationToken,
) -> Result<()> {
let cancel_token = cancel_token.clone();
-
- // TODO(Douwe): Make sure we generate the description here still:
-
self.push_message(message);
self.process_agent_response(false, cancel_token).await?;
Ok(())
@@ -442,7 +453,7 @@ impl CliSession {
// Track the current directory and last instruction in projects.json
if let Err(e) = crate::project_tracker::update_project_tracker(
Some(&content),
- self.session_id.as_deref(),
+ Some(&self.session_id),
) {
eprintln!("Warning: Failed to update project tracker with instruction: {}", e);
}
@@ -494,6 +505,10 @@ impl CliSession {
let current = output::get_theme();
let new_theme = match current {
+ output::Theme::Ansi => {
+ println!("Switching to Light theme");
+ output::Theme::Light
+ }
output::Theme::Light => {
println!("Switching to Dark theme");
output::Theme::Dark
@@ -502,10 +517,6 @@ impl CliSession {
println!("Switching to Ansi theme");
output::Theme::Ansi
}
- output::Theme::Ansi => {
- println!("Switching to Light theme");
- output::Theme::Light
- }
};
output::set_theme(new_theme);
continue;
@@ -545,21 +556,18 @@ impl CliSession {
save_history(&mut editor);
let config = Config::global();
- let mode = mode.to_lowercase();
-
- // Check if mode is valid
- if !["auto", "approve", "chat", "smart_approve"].contains(&mode.as_str()) {
- output::render_error(&format!(
- "Invalid mode '{}'. Mode must be one of: auto, approve, chat",
- mode
- ));
- continue;
- }
-
- config
- .set_param("GOOSE_MODE", Value::String(mode.to_string()))
- .unwrap();
- output::goose_mode_message(&format!("Goose mode set to '{}'", mode));
+ let mode = match GooseMode::from_str(&mode.to_lowercase()) {
+ Ok(mode) => mode,
+ Err(_) => {
+ output::render_error(&format!(
+ "Invalid mode '{}'. Mode must be one of: auto, approve, chat, smart_approve",
+ mode
+ ));
+ continue;
+ }
+ };
+ config.set_goose_mode(mode)?;
+ output::goose_mode_message(&format!("Goose mode set to '{:?}'", mode));
continue;
}
input::InputResult::Plan(options) => {
@@ -585,16 +593,14 @@ impl CliSession {
input::InputResult::Clear => {
save_history(&mut editor);
- if let Some(session_id) = &self.session_id {
- if let Err(e) = SessionManager::replace_conversation(
- session_id,
- &Conversation::default(),
- )
- .await
- {
- output::render_error(&format!("Failed to clear session: {}", e));
- continue;
- }
+ if let Err(e) = SessionManager::replace_conversation(
+ &self.session_id,
+ &Conversation::default(),
+ )
+ .await
+ {
+ output::render_error(&format!("Failed to clear session: {}", e));
+ continue;
}
self.messages.clear();
@@ -643,16 +649,16 @@ impl CliSession {
continue;
}
- InputResult::Summarize => {
+ InputResult::Compact => {
save_history(&mut editor);
- let prompt = "Are you sure you want to summarize this conversation? This will condense the message history.";
+ let prompt = "Are you sure you want to compact this conversation? This will condense the message history.";
let should_summarize =
match cliclack::confirm(prompt).initial_value(true).interact() {
Ok(choice) => choice,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
- false // If interrupted, set should_summarize to false
+ false
} else {
return Err(e.into());
}
@@ -660,90 +666,23 @@ impl CliSession {
};
if should_summarize {
- println!("{}", console::style("Summarizing conversation...").yellow());
+ self.push_message(Message::user().with_text(MANUAL_COMPACT_TRIGGER));
output::show_thinking();
-
- let (summarized_messages, _token_counts, summarization_usage) =
- goose::context_mgmt::compact_messages(
- &self.agent,
- &self.messages,
- false,
- )
+ self.process_agent_response(true, CancellationToken::default())
.await?;
-
- // Update the session messages with the summarized ones
- self.messages = summarized_messages.clone();
-
- // Persist the summarized messages and update session metadata
- if let Some(session_id) = &self.session_id {
- // Replace all messages with the summarized version
- SessionManager::replace_conversation(session_id, &summarized_messages)
- .await?;
-
- // Update session metadata with the new token counts from summarization
- if let Some(usage) = summarization_usage {
- let session =
- SessionManager::get_session(session_id, false).await?;
-
- // Update token counts with the summarization usage
- let summary_tokens = usage.usage.output_tokens.unwrap_or(0);
-
- // Update accumulated tokens (add the summarization cost)
- let accumulate = |a: Option, b: Option| -> Option {
- match (a, b) {
- (Some(x), Some(y)) => Some(x + y),
- _ => a.or(b),
- }
- };
-
- let accumulated_total = accumulate(
- session.accumulated_total_tokens,
- usage.usage.total_tokens,
- );
- let accumulated_input = accumulate(
- session.accumulated_input_tokens,
- usage.usage.input_tokens,
- );
- let accumulated_output = accumulate(
- session.accumulated_output_tokens,
- usage.usage.output_tokens,
- );
-
- SessionManager::update_session(session_id)
- .total_tokens(Some(summary_tokens))
- .input_tokens(None)
- .output_tokens(Some(summary_tokens))
- .accumulated_total_tokens(accumulated_total)
- .accumulated_input_tokens(accumulated_input)
- .accumulated_output_tokens(accumulated_output)
- .apply()
- .await?;
- }
- }
-
output::hide_thinking();
- println!(
- "{}",
- console::style("Conversation has been summarized.").green()
- );
- println!(
- "{}",
- console::style(
- "Key information has been preserved while reducing context length."
- )
- .green()
- );
} else {
- println!("{}", console::style("Summarization cancelled.").yellow());
+ println!("{}", console::style("Compaction cancelled.").yellow());
}
continue;
}
}
}
- if let Some(id) = &self.session_id {
- println!("Closing session. Session ID: {}", console::style(id).cyan());
- }
+ println!(
+ "Closing session. Session ID: {}",
+ console::style(&self.session_id).cyan()
+ );
Ok(())
}
@@ -787,12 +726,9 @@ impl CliSession {
self.run_mode = RunMode::Normal;
// set goose mode: auto if that isn't already the case
let config = Config::global();
- let curr_goose_mode =
- config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
- if curr_goose_mode != "auto" {
- config
- .set_param("GOOSE_MODE", Value::String("auto".to_string()))
- .unwrap();
+ let curr_goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
+ if curr_goose_mode != GooseMode::Auto {
+ config.set_goose_mode(GooseMode::Auto).unwrap();
}
// clear the messages before acting on the plan
@@ -807,10 +743,8 @@ impl CliSession {
output::hide_thinking();
// Reset run & goose mode
- if curr_goose_mode != "auto" {
- config
- .set_param("GOOSE_MODE", Value::String(curr_goose_mode.to_string()))
- .unwrap();
+ if curr_goose_mode != GooseMode::Auto {
+ config.set_goose_mode(curr_goose_mode)?;
}
} else {
// add the plan response (assistant message) & carry the conversation forward
@@ -843,18 +777,23 @@ impl CliSession {
) -> Result<()> {
let cancel_token_clone = cancel_token.clone();
- let session_config = self.session_id.as_ref().map(|session_id| SessionConfig {
- id: session_id.clone(),
- working_dir: std::env::current_dir().unwrap_or_default(),
+ // Cache the output format check to avoid repeated string comparisons in the hot loop
+ let is_json_mode = self.output_format == "json";
+
+ let session_config = SessionConfig {
+ id: self.session_id.clone(),
schedule_id: self.scheduled_job_id.clone(),
- execution_mode: None,
max_turns: self.max_turns,
retry_config: self.retry_config.clone(),
- });
+ };
+ let user_message = self
+ .messages
+ .last()
+ .ok_or_else(|| anyhow::anyhow!("No user message"))?;
let mut stream = self
.agent
.reply(
- self.messages.clone(),
+ user_message.clone(),
session_config.clone(),
Some(cancel_token.clone()),
)
@@ -972,11 +911,16 @@ impl CliSession {
);
}
}
+
self.messages.push(message.clone());
if interactive {output::hide_thinking()};
let _ = progress_bars.hide();
- output::render_message(&message, self.debug);
+
+ // Don't render in JSON mode
+ if !is_json_mode {
+ output::render_message(&message, self.debug);
+ }
}
}
Some(Ok(AgentEvent::McpNotification((_id, message)))) => {
@@ -1048,17 +992,21 @@ impl CliSession {
// TODO: proper display for subagent notifications
if interactive {
let _ = progress_bars.hide();
- println!("{}", console::style(&formatted_message).green().dim());
- } else {
+ if !is_json_mode {
+ println!("{}", console::style(&formatted_message).green().dim());
+ }
+ } else if !is_json_mode {
progress_bars.log(&formatted_message);
}
} else if let Some(ref notification_type) = message_notification_type {
if notification_type == TASK_EXECUTION_NOTIFICATION_TYPE {
if interactive {
let _ = progress_bars.hide();
- print!("{}", formatted_message);
- std::io::stdout().flush().unwrap();
- } else {
+ if !is_json_mode {
+ print!("{}", formatted_message);
+ std::io::stdout().flush().unwrap();
+ }
+ } else if !is_json_mode {
print!("{}", formatted_message);
std::io::stdout().flush().unwrap();
}
@@ -1137,7 +1085,29 @@ impl CliSession {
}
}
}
- println!();
+
+ // Output JSON if requested
+ if is_json_mode {
+ let metadata = match SessionManager::get_session(&self.session_id, false).await {
+ Ok(session) => JsonMetadata {
+ total_tokens: session.total_tokens,
+ status: "completed".to_string(),
+ },
+ Err(_) => JsonMetadata {
+ total_tokens: None,
+ status: "completed".to_string(),
+ },
+ };
+
+ let json_output = JsonOutput {
+ messages: self.messages.messages().to_vec(),
+ metadata,
+ };
+
+ println!("{}", serde_json::to_string_pretty(&json_output)?);
+ } else {
+ println!();
+ }
Ok(())
}
@@ -1299,16 +1269,13 @@ impl CliSession {
);
}
- pub async fn get_metadata(&self) -> Result {
- match &self.session_id {
- Some(id) => SessionManager::get_session(id, false).await,
- None => Err(anyhow::anyhow!("No session available")),
- }
+ pub async fn get_session(&self) -> Result {
+ SessionManager::get_session(&self.session_id, false).await
}
// Get the session's total token usage
pub async fn get_total_token_usage(&self) -> Result