mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Merge origin/main into tui_gem
Resolved conflicts: - Cargo.lock: regenerated with cargo update - crates/goose-server/src/routes/agent.rs: merged call_tool implementations (kept tui_gem's dispatch_tool_call with session context, added read_resource from main) - crates/goose-tui: fixed CallToolResult API changes (access .content field)
This commit is contained in:
@@ -16,9 +16,7 @@ body:
|
||||
|
||||
🪄 **What Happens After?**
|
||||
- If accepted, we'll publish your recipe to the [goose recipes cookbook](https://block.github.io/goose/recipes)
|
||||
- You'll receive OpenRouter **LLM API credits** as a thank you!
|
||||
- Your GitHub handle will be displayed and linked on the recipe card
|
||||
- If you provide an email below, we'll email you your credits when your recipe is approved and merged.
|
||||
- If the YAML has any issues, goose will comment with validation errors so you can fix and resubmit.
|
||||
|
||||
🧪 **Pro Tip:** You can test your recipe locally in your terminal with:
|
||||
@@ -86,15 +84,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: email
|
||||
attributes:
|
||||
label: Your Email (optional)
|
||||
description: If your recipe is approved, we'll email your LLM API credits here.
|
||||
placeholder: yourname@example.com
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
||||
@@ -31,6 +31,3 @@ Before:
|
||||
|
||||
After:
|
||||
|
||||
### Submitting a Recipe?
|
||||
<!-- For Recipe Cookbook Submissions ONLY: Include your email below to receive $10 OpenRouter credits once approved and merged -->
|
||||
**Email**:
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
import re
|
||||
import email_validator
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail
|
||||
from python_http_client.exceptions import HTTPError
|
||||
|
||||
|
||||
def main():
|
||||
# Environment variable validation
|
||||
required_envs = ["GITHUB_TOKEN", "GITHUB_SHA", "GITHUB_REPOSITORY", "PROVISIONING_API_KEY", "EMAIL_API_KEY"]
|
||||
missing = [env for env in required_envs if env not in os.environ]
|
||||
if missing:
|
||||
print(f"❌ Missing environment variables: {', '.join(missing)}")
|
||||
exit(2)
|
||||
|
||||
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
|
||||
GITHUB_SHA = os.environ["GITHUB_SHA"]
|
||||
REPO_NAME = os.environ["GITHUB_REPOSITORY"]
|
||||
PROVISIONING_API_KEY = os.environ["PROVISIONING_API_KEY"]
|
||||
SENDGRID_API_KEY = os.environ["EMAIL_API_KEY"]
|
||||
|
||||
pr_number = get_pr_number_from_sha()
|
||||
pr_data = fetch_pr_body(pr_number, GITHUB_TOKEN, REPO_NAME)
|
||||
pr_body = pr_data.get("body", "")
|
||||
|
||||
email = extract_email(pr_body, REPO_NAME, pr_number, GITHUB_TOKEN)
|
||||
print(f"📬 Found email: {email}")
|
||||
|
||||
try:
|
||||
api_key = provision_api_key(PROVISIONING_API_KEY)
|
||||
print("✅ API key generated!")
|
||||
|
||||
if not send_email(email, api_key, SENDGRID_API_KEY):
|
||||
print("❌ Email failed to send. Exiting without PR comment.")
|
||||
exit(2)
|
||||
|
||||
comment_on_pr(GITHUB_TOKEN, REPO_NAME, pr_number, email)
|
||||
|
||||
except Exception as err:
|
||||
print(f"❌ An error occurred: {err}")
|
||||
exit(2)
|
||||
|
||||
def get_pr_number_from_sha():
|
||||
token = os.getenv("GITHUB_TOKEN")
|
||||
repo = os.getenv("GITHUB_REPOSITORY")
|
||||
sha = os.getenv("GITHUB_SHA")
|
||||
|
||||
url = f"https://api.github.com/repos/{repo}/commits/{sha}/pulls"
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/vnd.github.groot-preview+json"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
pr_data = response.json()
|
||||
if pr_data:
|
||||
return pr_data[0]["number"]
|
||||
else:
|
||||
raise Exception("No PR found for this SHA")
|
||||
|
||||
def fetch_pr_body(pr_number, github_token, repo_full_name):
|
||||
print("🔍 Fetching PR body...")
|
||||
pr_url = f"https://api.github.com/repos/{repo_full_name}/pulls/{pr_number}"
|
||||
try:
|
||||
pr_resp = requests.get(
|
||||
pr_url,
|
||||
headers={"Authorization": f"Bearer {github_token}"}
|
||||
)
|
||||
pr_resp.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print("❌ Failed to fetch PR body:", str(e))
|
||||
raise
|
||||
return pr_resp.json()
|
||||
|
||||
def extract_email_from_text(text):
|
||||
email_match = re.search(r"\*\*Email\*\*:\s*([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})", text)
|
||||
if email_match:
|
||||
return email_match.group(1)
|
||||
email_match = re.search(r"[Ee]mail:\s*([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})", text)
|
||||
if email_match:
|
||||
return email_match.group(1)
|
||||
email_match = re.search(r"\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b", text)
|
||||
if email_match:
|
||||
return email_match.group(1)
|
||||
return None
|
||||
|
||||
def fetch_pr_comments(repo_full_name, pr_number, github_token):
|
||||
comments_url = f"https://api.github.com/repos/{repo_full_name}/issues/{pr_number}/comments"
|
||||
try:
|
||||
comments_resp = requests.get(
|
||||
comments_url,
|
||||
headers={"Authorization": f"Bearer {github_token}"}
|
||||
)
|
||||
comments_resp.raise_for_status()
|
||||
return comments_resp.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"⚠️ Failed to fetch PR comments: {e}")
|
||||
return []
|
||||
|
||||
def validate_email_address(email):
|
||||
try:
|
||||
valid_email = email_validator.validate_email(email)
|
||||
normalized_email = valid_email.email
|
||||
print(f"✅ Email validation passed: {normalized_email}")
|
||||
return normalized_email
|
||||
except email_validator.EmailNotValidError as e:
|
||||
print(f"❌ Email validation failed: {e}")
|
||||
return None
|
||||
|
||||
def extract_email(pr_body, repo_full_name, pr_number, github_token):
|
||||
print("🔍 Searching for email in PR body...")
|
||||
email = extract_email_from_text(pr_body)
|
||||
if email:
|
||||
print(f"📧 Found email in PR body: {email}")
|
||||
validated_email = validate_email_address(email)
|
||||
if validated_email:
|
||||
return validated_email
|
||||
else:
|
||||
print("⚠️ Email in PR body is invalid, checking comments...")
|
||||
|
||||
print("🔍 No valid email found in PR body, checking comments...")
|
||||
comments = fetch_pr_comments(repo_full_name, pr_number, github_token)
|
||||
for comment in comments:
|
||||
comment_body = comment.get("body", "")
|
||||
email = extract_email_from_text(comment_body)
|
||||
if email:
|
||||
print(f"📧 Found email in comment by {comment.get('user', {}).get('login', 'unknown')}: {email}")
|
||||
validated_email = validate_email_address(email)
|
||||
if validated_email:
|
||||
return validated_email
|
||||
else:
|
||||
print("⚠️ Email in comment is invalid, continuing search...")
|
||||
|
||||
print("❌ No valid email found in PR body or comments. Skipping key issuance.")
|
||||
exit(2)
|
||||
|
||||
def provision_api_key(provisioning_api_key):
|
||||
print("🔐 Creating OpenRouter key...")
|
||||
try:
|
||||
key_resp = requests.post(
|
||||
"https://openrouter.ai/api/v1/keys",
|
||||
headers={
|
||||
"Authorization": f"Bearer {provisioning_api_key}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"name": "goose contributor",
|
||||
"label": "goose-cookbook",
|
||||
"limit": 10.0
|
||||
}
|
||||
)
|
||||
key_resp.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print("❌ Failed to provision API key:", str(e))
|
||||
raise
|
||||
key = key_resp.json().get("key")
|
||||
if not key:
|
||||
print("❌ API response did not include a key.")
|
||||
exit(2)
|
||||
return key
|
||||
|
||||
def send_email(email, api_key, sendgrid_api_key):
|
||||
print("📤 Sending email via SendGrid...")
|
||||
try:
|
||||
sg = SendGridAPIClient(sendgrid_api_key)
|
||||
from_email = "goose team <goose@opensource.block.xyz>"
|
||||
subject = "🎉 Your goose contributor API key"
|
||||
html_content = f"""
|
||||
<p>Thank you for contributing to the <strong>goose recipe cookbook</strong>!</p>
|
||||
<p>🎉 Here's your <strong>$10 OpenRouter API key</strong>:</p>
|
||||
<pre style="background-color:#f4f4f4;padding:10px;border-radius:6px;"><code>{api_key}</code></pre>
|
||||
<p>To use this in goose (CLI or Desktop):</p>
|
||||
<ul>
|
||||
<li>Go to your <strong>Provider Settings</strong></li>
|
||||
<li>Select <strong>OpenRouter</strong> from the provider list</li>
|
||||
<li>Paste your API key</li>
|
||||
</ul>
|
||||
<p>📚 Full setup instructions:<br>
|
||||
<a href="https://block.github.io/goose/docs/getting-started/providers/#configure-provider">
|
||||
https://block.github.io/goose/docs/getting-started/providers/#configure-provider</a></p>
|
||||
<p>Happy coding!<br>– the goose team</p>
|
||||
"""
|
||||
message = Mail(
|
||||
from_email=from_email,
|
||||
to_emails=email,
|
||||
subject=subject,
|
||||
html_content=html_content
|
||||
)
|
||||
response = sg.send(message)
|
||||
print(f"✅ Email sent successfully! Status code: {response.status_code}")
|
||||
if response.status_code >= 300:
|
||||
print(f"⚠️ Warning: Unexpected status code {response.status_code}")
|
||||
print(f"Response body: {response.body}")
|
||||
return False
|
||||
return True
|
||||
|
||||
except HTTPError as e:
|
||||
print(f"❌ SendGrid HTTP error {e.status_code}: {e.body}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error sending email: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
def comment_on_pr(github_token, repo_full_name, pr_number, email):
|
||||
print("💬 Commenting on PR...")
|
||||
comment_url = f"https://api.github.com/repos/{repo_full_name}/issues/{pr_number}/comments"
|
||||
try:
|
||||
comment_resp = requests.post(
|
||||
comment_url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {github_token}",
|
||||
"Accept": "application/vnd.github+json"
|
||||
},
|
||||
json={
|
||||
"body": f"✅ $10 OpenRouter API key sent to `{email}`. Thanks for your contribution to the goose cookbook!"
|
||||
}
|
||||
)
|
||||
comment_resp.raise_for_status()
|
||||
print("✅ Confirmation comment added to PR.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print("❌ Failed to comment on PR:", str(e))
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,200 @@
|
||||
name: goose Issue Solver
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to solve'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
GOOSE_RECIPE: |
|
||||
version: "1.0.0"
|
||||
title: "Solve GitHub Issue"
|
||||
description: "Solve GitHub issue #${ISSUE_NUMBER}"
|
||||
|
||||
extensions:
|
||||
- type: builtin
|
||||
name: developer
|
||||
- type: platform
|
||||
name: todo
|
||||
|
||||
instructions: |
|
||||
Principles:
|
||||
- Extract all requirements before coding. Missing one means failure.
|
||||
- Understand before changing. Research the code first.
|
||||
- Follow existing patterns and AGENTS.md if it exists.
|
||||
- Stop when requirements are met. Nothing more.
|
||||
- Verify through deterministic means.
|
||||
- Your context degrades. The TODO is your memory. Update it after each step.
|
||||
|
||||
prompt: |
|
||||
Solve GitHub issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}
|
||||
|
||||
The issue is saved at /tmp/issue.json
|
||||
|
||||
Trigger comment:
|
||||
${TRIGGER_COMMENT}
|
||||
|
||||
Write this to your TODO immediately and update as you progress:
|
||||
|
||||
## Phase 1: Understand
|
||||
- [ ] Read /tmp/issue.json
|
||||
- [ ] Write all requirements (explicit + implicit) to /tmp/requirements.md
|
||||
- [ ] Read AGENTS.md if it exists
|
||||
|
||||
## Phase 2: Research
|
||||
- [ ] Explore codebase with analyze and rg
|
||||
- [ ] Identify files that need to change
|
||||
- [ ] Update TODO with findings
|
||||
|
||||
## Phase 3: Plan
|
||||
- [ ] Decide on implementation approach
|
||||
- [ ] For nontrivial issues, use subagents to evaluate architecture or implementation choices
|
||||
- [ ] Update TODO with specific changes to make
|
||||
|
||||
## Phase 4: Implement
|
||||
- [ ] Implement minimal fix per /tmp/requirements.md
|
||||
- [ ] Before adding anything, check: is it in the requirements? If not, don't.
|
||||
- [ ] No .github/, no lock files, no secrets
|
||||
|
||||
## Phase 5: Verify
|
||||
- [ ] source bin/activate-hermit
|
||||
- [ ] cargo check
|
||||
- [ ] cargo test (affected crates)
|
||||
- [ ] cargo fmt
|
||||
- [ ] ./scripts/clippy-lint.sh
|
||||
- [ ] Fix failures, retry up to 3 times
|
||||
|
||||
## Phase 6: Confirm (MANDATORY)
|
||||
- [ ] Reread /tmp/issue.json
|
||||
- [ ] Reread /tmp/requirements.md
|
||||
- [ ] Confirm all requirements met, nothing extra added
|
||||
- [ ] Write a summary of changes to /tmp/issue_summary.txt
|
||||
|
||||
The Phase 6 "Reread" steps MUST appear in your updated TODO. Skipping them is failure.
|
||||
|
||||
Only write /tmp/issue_summary.txt if the fix is complete and verified.
|
||||
Run `git add` for any new files required for the fix.
|
||||
Do NOT commit. Leave changes in the working directory for the workflow to handle.
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
concurrency:
|
||||
group: goose-issue-${{ github.event.issue.number || github.event.inputs.issue_number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
solve-issue:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(!github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/goose') &&
|
||||
contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association))
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
container:
|
||||
image: ghcr.io/block/goose:latest
|
||||
options: --user root
|
||||
env:
|
||||
GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'openai' }}
|
||||
GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'gpt-5.1' }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
HOME: /tmp/goose-home
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y jq gettext curl build-essential pkg-config libssl-dev
|
||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
apt-get update
|
||||
apt-get install -y gh
|
||||
|
||||
- name: Get issue details (issue_comment)
|
||||
if: github.event_name == 'issue_comment'
|
||||
env:
|
||||
ISSUE_JSON: ${{ toJSON(github.event.issue) }}
|
||||
run: printenv ISSUE_JSON > /tmp/issue.json
|
||||
|
||||
- name: Get issue details (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh api /repos/${{ github.repository }}/issues/${{ github.event.inputs.issue_number }} > /tmp/issue.json
|
||||
|
||||
- name: Set issue outputs
|
||||
id: issue
|
||||
run: |
|
||||
echo "number=$(jq -r '.number' /tmp/issue.json)" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "title<<TITLE_EOF" >> $GITHUB_OUTPUT
|
||||
jq -r '.title' /tmp/issue.json >> $GITHUB_OUTPUT
|
||||
echo "TITLE_EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run goose
|
||||
id: goose
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ steps.issue.outputs.number }}
|
||||
ISSUE_TITLE: ${{ steps.issue.outputs.title }}
|
||||
TRIGGER_COMMENT: ${{ github.event.comment.body || 'Triggered via workflow_dispatch' }}
|
||||
run: |
|
||||
mkdir -p $HOME/.local/share/goose/sessions
|
||||
mkdir -p $HOME/.config/goose
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
echo "$GOOSE_RECIPE" | envsubst '$ISSUE_NUMBER $ISSUE_TITLE $TRIGGER_COMMENT' > /tmp/recipe.yaml
|
||||
|
||||
goose run --recipe /tmp/recipe.yaml
|
||||
|
||||
if [ -n "$(git status --porcelain)" ] && [ -f /tmp/issue_summary.txt ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "summary<<SUMMARY_EOF" >> $GITHUB_OUTPUT
|
||||
cat /tmp/issue_summary.txt >> $GITHUB_OUTPUT
|
||||
echo "SUMMARY_EOF" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Verify no workflow changes
|
||||
if: steps.goose.outputs.has_changes == 'true'
|
||||
run: |
|
||||
if git diff --name-only | grep -q "^\.github/"; then
|
||||
echo "::error::Changes to .github/ are not allowed"
|
||||
git checkout -- .github/
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.goose.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # pin@v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "fix: ${{ steps.issue.outputs.title }}"
|
||||
title: "fix: ${{ steps.issue.outputs.title }}"
|
||||
branch: goose/issue-${{ steps.issue.outputs.number }}
|
||||
delete-branch: true
|
||||
draft: true
|
||||
labels: goose-generated
|
||||
body: |
|
||||
Closes #${{ steps.issue.outputs.number }}
|
||||
|
||||
## Summary
|
||||
${{ steps.goose.outputs.summary || 'See commits for details.' }}
|
||||
|
||||
---
|
||||
*Generated by goose Issue Solver*
|
||||
@@ -1,71 +0,0 @@
|
||||
name: Send API Key on PR Merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
paths:
|
||||
- 'documentation/src/pages/recipes/data/recipes/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
send-api-key:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo at merge commit
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if recipe files were added or modified in merged PR
|
||||
id: recipe_changes
|
||||
run: |
|
||||
set -e
|
||||
echo "🔍 Checking if recipe files were added or modified in merged PR..."
|
||||
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
echo "Merge commit: $MERGE_COMMIT"
|
||||
|
||||
# Get parent commit of the merge
|
||||
PARENT_COMMIT=$(git rev-parse "$MERGE_COMMIT^1")
|
||||
echo "Parent commit: $PARENT_COMMIT"
|
||||
|
||||
# Get list of added or modified files in the PR (ignore deletions)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM "$PARENT_COMMIT" "$MERGE_COMMIT")
|
||||
|
||||
echo "Files added/modified in merged PR:"
|
||||
echo "$CHANGED_FILES"
|
||||
echo ""
|
||||
|
||||
# Detect recipe changes only
|
||||
if echo "$CHANGED_FILES" | grep -q "^documentation/src/pages/recipes/data/recipes/"; then
|
||||
echo "recipe_files_changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "✅ Recipe files were added/modified in merged PR - proceeding with API key sending"
|
||||
else
|
||||
echo "recipe_files_changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ No recipe files were added/modified in merged PR - skipping API key sending"
|
||||
fi
|
||||
|
||||
- name: Set up Python
|
||||
if: steps.recipe_changes.outputs.recipe_files_changed == 'true'
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies and run email script
|
||||
if: steps.recipe_changes.outputs.recipe_files_changed == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
PROVISIONING_API_KEY: ${{ secrets.PROVISIONING_API_KEY }}
|
||||
EMAIL_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
|
||||
run: |
|
||||
pip install requests sendgrid email-validator
|
||||
python .github/scripts/send_key.py
|
||||
@@ -1,265 +0,0 @@
|
||||
name: Validate Recipe PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- 'documentation/src/pages/recipes/data/recipes/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
validate-recipe:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
PROVIDER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install and Configure goose
|
||||
run: |
|
||||
mkdir -p /home/runner/.local/bin
|
||||
curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh \
|
||||
| CONFIGURE=false INSTALL_PATH=/home/runner/.local/bin bash
|
||||
echo "/home/runner/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
mkdir -p ~/.config/goose
|
||||
cat <<EOF > ~/.config/goose/config.yaml
|
||||
GOOSE_PROVIDER: openrouter
|
||||
GOOSE_MODEL: "anthropic/claude-sonnet-4"
|
||||
keyring: false
|
||||
EOF
|
||||
|
||||
- name: Check if recipe files changed in this push
|
||||
id: recipe_changes
|
||||
run: |
|
||||
set -e
|
||||
echo "🔍 Checking if recipe files were modified in this push..."
|
||||
|
||||
# Get the list of changed files in this specific push (added/modified only, not deleted)
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.action }}" = "synchronize" ]; then
|
||||
# For synchronize events, check files changed since the previous commit
|
||||
echo "📝 Synchronize event - checking files changed since previous commit"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }})
|
||||
else
|
||||
# For opened/reopened, check all files in the PR (compare PR head against base)
|
||||
echo "📝 PR opened/reopened - checking all files in PR"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
||||
fi
|
||||
|
||||
echo "Changed files in this push:"
|
||||
echo "$CHANGED_FILES"
|
||||
echo ""
|
||||
|
||||
# Check if any recipe files were changed
|
||||
if echo "$CHANGED_FILES" | grep -q "^documentation/src/pages/recipes/data/recipes/"; then
|
||||
echo "recipe_files_changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "✅ Recipe files were modified in this push - proceeding with validation"
|
||||
else
|
||||
echo "recipe_files_changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ No recipe files were modified in this push - skipping validation"
|
||||
fi
|
||||
|
||||
- name: Find recipe files in PR (new or modified)
|
||||
id: find_changed_recipes
|
||||
if: steps.recipe_changes.outputs.recipe_files_changed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
echo "Looking for recipe files in PR (new or modified)..."
|
||||
|
||||
# Get the list of changed/new files in this PR (added/modified only, not deleted)
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.action }}" = "synchronize" ]; then
|
||||
# For synchronize events, check files changed since the previous commit
|
||||
echo "📝 Synchronize event - checking files changed/added since previous commit"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }})
|
||||
else
|
||||
# For opened/reopened, check all files in the PR (compare PR head against base)
|
||||
echo "📝 PR opened/reopened - checking all new/modified files in PR"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
||||
fi
|
||||
|
||||
# Filter for recipe files only that were changed or added
|
||||
RECIPE_FILES=$(echo "$CHANGED_FILES" | grep "^documentation/src/pages/recipes/data/recipes/" | grep -E "\.(yaml|yml)$" || true)
|
||||
|
||||
if [ -z "$RECIPE_FILES" ]; then
|
||||
echo "No changed recipe files found in PR"
|
||||
echo "validation_status=no_files" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found changed recipe files:"
|
||||
echo "$RECIPE_FILES"
|
||||
|
||||
# Save recipe file paths for validation step
|
||||
echo "$RECIPE_FILES" > /tmp/changed_recipe_files.txt
|
||||
|
||||
- name: Validate changed recipe files
|
||||
id: validate
|
||||
if: steps.recipe_changes.outputs.recipe_files_changed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
# Read the list of changed recipe files
|
||||
RECIPE_FILES=$(cat /tmp/changed_recipe_files.txt)
|
||||
|
||||
ALL_VALID=true
|
||||
VALIDATION_OUTPUT=""
|
||||
|
||||
# First pass: Basic YAML validation
|
||||
while IFS= read -r RECIPE_FILE; do
|
||||
if [ -f "$RECIPE_FILE" ]; then
|
||||
BASE_RECIPE_FILENAME=$(basename "$RECIPE_FILE")
|
||||
echo "🔍 Validating: $BASE_RECIPE_FILENAME"
|
||||
if OUTPUT=$(goose recipe validate "$RECIPE_FILE" 2>&1); then
|
||||
echo "✅ Valid: $BASE_RECIPE_FILENAME"
|
||||
VALIDATION_OUTPUT="${VALIDATION_OUTPUT}✅ $BASE_RECIPE_FILENAME: VALID\n"
|
||||
else
|
||||
echo "❌ Invalid: $BASE_RECIPE_FILENAME"
|
||||
echo "$OUTPUT"
|
||||
VALIDATION_OUTPUT="${VALIDATION_OUTPUT}❌ $BASE_RECIPE_FILENAME: INVALID\n\`\`\`\n$OUTPUT\n\`\`\`\n"
|
||||
ALL_VALID=false
|
||||
fi
|
||||
fi
|
||||
done < /tmp/changed_recipe_files.txt
|
||||
|
||||
# Second pass: Check for duplicate filenames
|
||||
if [ "$ALL_VALID" = true ]; then
|
||||
echo "🔍 Checking for duplicate filenames..."
|
||||
|
||||
# Check for duplicate filenames first
|
||||
SEEN_FILENAMES=""
|
||||
while IFS= read -r RECIPE_FILE; do
|
||||
if [ -f "$RECIPE_FILE" ]; then
|
||||
FILENAME=$(basename "$RECIPE_FILE" .yaml)
|
||||
FILENAME=$(basename "$FILENAME" .yml)
|
||||
|
||||
echo "📋 Checking filename: '$FILENAME'"
|
||||
|
||||
# Check if we've seen this filename before in this PR
|
||||
if echo "$SEEN_FILENAMES" | grep -q "^$FILENAME$"; then
|
||||
echo "❌ Duplicate filename '$FILENAME' found in this PR"
|
||||
VALIDATION_OUTPUT="${VALIDATION_OUTPUT}❌ Duplicate filename '$FILENAME' found in this PR\n"
|
||||
ALL_VALID=false
|
||||
else
|
||||
SEEN_FILENAMES="$SEEN_FILENAMES\n$FILENAME"
|
||||
fi
|
||||
|
||||
# Check if this is a new file or an update to existing file
|
||||
# Get list of changed files in this PR compared to base branch
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep "^$RECIPE_FILE$" || true)
|
||||
EXISTING_FILES=$(find documentation/src/pages/recipes/data/recipes/ -name "$FILENAME.yaml" -o -name "$FILENAME.yml" | grep -v "^$RECIPE_FILE$" || true)
|
||||
|
||||
if [ -n "$EXISTING_FILES" ] && [ -z "$CHANGED_FILES" ]; then
|
||||
# File exists in repo but is not being modified - this is a new duplicate
|
||||
echo "❌ Recipe filename '$FILENAME' already exists:"
|
||||
echo "$EXISTING_FILES"
|
||||
VALIDATION_OUTPUT="${VALIDATION_OUTPUT}❌ $RECIPE_FILE: Filename '$FILENAME' already exists in: $EXISTING_FILES\n"
|
||||
ALL_VALID=false
|
||||
elif [ -n "$EXISTING_FILES" ] && [ -n "$CHANGED_FILES" ]; then
|
||||
# File exists and is being modified - this is an update
|
||||
echo "✅ Updating existing recipe: '$FILENAME'"
|
||||
else
|
||||
# File doesn't exist - this is a new recipe
|
||||
echo "✅ New recipe filename '$FILENAME' is unique"
|
||||
fi
|
||||
|
||||
echo "✅ Filename '$FILENAME' validation complete"
|
||||
fi
|
||||
done < /tmp/changed_recipe_files.txt
|
||||
fi
|
||||
|
||||
# Save validation output for use in comment
|
||||
echo "$VALIDATION_OUTPUT" > /tmp/validation_output.txt
|
||||
|
||||
if [ "$ALL_VALID" = true ]; then
|
||||
echo "validation_status=valid" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "validation_status=invalid" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Comment validation results
|
||||
if: steps.recipe_changes.outputs.recipe_files_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const status = '${{ steps.validate.outputs.validation_status }}';
|
||||
|
||||
let comment;
|
||||
if (status === 'no_files') {
|
||||
comment = `❌ **Recipe Validation Failed**
|
||||
|
||||
No recipe files found in the correct location!
|
||||
|
||||
📁 **Please add your recipe to**: \`documentation/src/pages/recipes/data/recipes/your-recipe-id.yaml\`
|
||||
|
||||
**Example**: If your recipe ID is \`web-scraper\`, create:
|
||||
\`documentation/src/pages/recipes/data/recipes/web-scraper.yaml\``;
|
||||
} else if (status === 'valid') {
|
||||
comment = `✅ **Recipe Validation Passed**
|
||||
|
||||
Your recipe(s) are valid and ready for review!
|
||||
|
||||
🔍 **Next Steps**:
|
||||
1. Our team will review your recipe
|
||||
2. If approved, we'll run a security scan
|
||||
3. Once merged, you'll receive $10 in OpenRouter credits (if email provided)
|
||||
|
||||
Thanks for contributing to the goose Recipe Cookbook! 🎉`;
|
||||
} else {
|
||||
// Read validation details from file
|
||||
let validationDetails = '';
|
||||
try {
|
||||
validationDetails = fs.readFileSync('/tmp/validation_output.txt', 'utf8');
|
||||
} catch (e) {
|
||||
validationDetails = 'See workflow logs for details.';
|
||||
}
|
||||
|
||||
comment = `❌ **Recipe Validation Failed**
|
||||
|
||||
Please fix the validation errors and push your changes:
|
||||
|
||||
${validationDetails}
|
||||
|
||||
📚 Check our [Recipe Guide](https://block.github.io/goose/recipes) for help with the correct format.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
- name: Set validation status
|
||||
if: always()
|
||||
run: |
|
||||
# Check if recipe files were changed in this PR
|
||||
if [ "${{ steps.recipe_changes.outputs.recipe_files_changed }}" = "false" ]; then
|
||||
# No recipe files were modified in this PR - validation skipped
|
||||
echo "ℹ️ No recipe files in PR - validation skipped"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VALIDATION_STATUS="${{ steps.validate.outputs.validation_status }}"
|
||||
if [ "$VALIDATION_STATUS" = "valid" ]; then
|
||||
echo "✅ All recipes are valid"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Recipe validation failed"
|
||||
exit 1
|
||||
fi
|
||||
Generated
+76
-48
@@ -1638,9 +1638,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -2128,23 +2128,55 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
|
||||
dependencies = [
|
||||
"convert_case 0.7.1",
|
||||
"convert_case 0.10.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
@@ -2863,7 +2895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
@@ -2905,6 +2937,7 @@ dependencies = [
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry_sdk",
|
||||
"paste",
|
||||
"posthog-rs",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.12",
|
||||
@@ -2946,7 +2979,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-bench"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2969,7 +3002,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-cli"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"agent-client-protocol",
|
||||
"anstream",
|
||||
@@ -3023,7 +3056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-client"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"eventsource-stream",
|
||||
@@ -3040,7 +3073,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-mcp"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3105,7 +3138,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-server"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3124,6 +3157,7 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
"reqwest 0.12.12",
|
||||
"rmcp 0.9.1",
|
||||
"rustls 0.23.31",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3149,7 +3183,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-test"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"serde_json",
|
||||
@@ -3157,7 +3191,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-tui"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"ansi-to-tui",
|
||||
"anyhow",
|
||||
@@ -3886,9 +3920,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a"
|
||||
checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c"
|
||||
dependencies = [
|
||||
"darling 0.20.10",
|
||||
"indoc",
|
||||
@@ -4543,23 +4577,6 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
@@ -5385,6 +5402,21 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "posthog-rs"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "610d236077c94c96194b0d2b40989bda4bf8faf60e15a0da6ef8ffd4c78f6c1c"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"reqwest 0.11.27",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
@@ -5955,6 +5987,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 0.25.4",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
@@ -6225,6 +6258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -7589,16 +7623,6 @@ dependencies = [
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
@@ -7650,9 +7674,11 @@ checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rustls 0.23.31",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tungstenite 0.28.0",
|
||||
]
|
||||
|
||||
@@ -8043,8 +8069,9 @@ dependencies = [
|
||||
"http 1.2.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand 0.9.2",
|
||||
"rustls 0.23.31",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.12",
|
||||
"utf-8",
|
||||
@@ -8252,6 +8279,7 @@ checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
authors = ["Block <ai-oss-tools@block.xyz>"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/block/goose"
|
||||
|
||||
@@ -187,3 +187,13 @@ Core Maintainers and Maintainers are listed in the main goose repository's [MAIN
|
||||
* **Quality**: Thoughtful review while avoiding bureaucracy
|
||||
|
||||
We believe this balance enables goose to remain innovative while building a strong, engaged community around the shared goal of creating the most hackable, user-controlled AI agent available.
|
||||
|
||||
# General Project Policies
|
||||
|
||||
Founded by Block, goose has been established as a Series of LF Projects, LLC. Policies applicable to goose and participants in the goose project, including guidelines on the usage of trademarks, are located at [https://lfprojects.org/policies/](https://lfprojects.org/policies/). Governance changes approved as per the provisions of this governance document must also be approved by LF Projects, LLC.
|
||||
|
||||
goose participants acknowledge that the copyright in all new contributions will be retained by the copyright holder as independent works of authorship and that no contributor or copyright holder will be required to assign copyrights to the project.
|
||||
Except as described below, all code and specification contributions to the project must be made using the Apache License, Version 2.0 available at (the “Project License”).
|
||||
|
||||
All outbound code and specifications will be made available under the Project License. The Core Maintainers may approve the use of an alternative open license or licenses for inbound or outbound contributions on an exception basis.
|
||||
All documentation (excluding specifications) will be made available under the Creative Commons Attribution 4.0 International license, available at: https://creativecommons.org/licenses/by/4.0.
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Evaluation for DeveloperImage {
|
||||
if let MessageContent::ToolResponse(tool_resp) = content {
|
||||
if let Ok(result) = &tool_resp.tool_result {
|
||||
// Check each item in the result list
|
||||
for item in result {
|
||||
for item in &result.content {
|
||||
if let Some(image) = item.as_image() {
|
||||
// Image content already contains mime_type and data
|
||||
if image.mime_type.starts_with("image/")
|
||||
|
||||
@@ -11,7 +11,7 @@ use goose::mcp_utils::ToolResult;
|
||||
use goose::providers::create;
|
||||
use goose::session::session_manager::SessionType;
|
||||
use goose::session::SessionManager;
|
||||
use rmcp::model::{Content, RawContent, ResourceContents, Role};
|
||||
use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
@@ -74,8 +74,8 @@ fn extract_tool_locations(
|
||||
.and_then(|c| c.as_str());
|
||||
|
||||
// Extract line numbers from the response content
|
||||
if let Ok(content_items) = &tool_response.tool_result {
|
||||
for content in content_items {
|
||||
if let Ok(result) = &tool_response.tool_result {
|
||||
for content in &result.content {
|
||||
if let RawContent::Text(text_content) = &content.raw {
|
||||
let text = &text_content.text;
|
||||
|
||||
@@ -491,9 +491,10 @@ impl GooseAcpAgent {
|
||||
}
|
||||
|
||||
/// Build tool call content from tool result
|
||||
fn build_tool_call_content(tool_result: &ToolResult<Vec<Content>>) -> Vec<ToolCallContent> {
|
||||
fn build_tool_call_content(tool_result: &ToolResult<CallToolResult>) -> Vec<ToolCallContent> {
|
||||
match tool_result {
|
||||
Ok(content_items) => content_items
|
||||
Ok(result) => result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| match &content.raw {
|
||||
RawContent::Text(val) => Some(ToolCallContent::Content {
|
||||
|
||||
@@ -576,7 +576,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
|
||||
let models_res = {
|
||||
let temp_model_config = ModelConfig::new(&provider_meta.default_model)?;
|
||||
let temp_provider = create(provider_name, temp_model_config).await?;
|
||||
temp_provider.fetch_supported_models().await
|
||||
temp_provider.fetch_recommended_models().await
|
||||
};
|
||||
spin.stop(style("Model fetch complete").green());
|
||||
|
||||
@@ -1891,7 +1891,7 @@ fn add_provider() -> anyhow::Result<()> {
|
||||
.mask('▪')
|
||||
.interact()?;
|
||||
|
||||
let models_input: String = cliclack::input("Available models (seperate with commas):")
|
||||
let models_input: String = cliclack::input("Available models (separate with commas):")
|
||||
.placeholder("model-a, model-b, model-c")
|
||||
.validate(|input: &String| {
|
||||
if input.trim().is_empty() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"74a3cc715c4c65dcae4ecec084d962947e4ce3cf02a8b639ee23b080e88bf6ec": {
|
||||
"1b90ffd4b16eab7aef1c24e9b61f6b6cc6f0d9b04e8a4f4344454a45b0793284": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_8_0",
|
||||
"id": "msg_20251211_27_0",
|
||||
"role": "user",
|
||||
"created": 1763989805,
|
||||
"created": 1765495263,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -19,13 +19,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_b4135f6c-9a29-4904-a59f-632ea849ef5a",
|
||||
"id": "msg_9076da8a-a3b1-4cfc-b5d9-0ffc2a7879c9",
|
||||
"role": "assistant",
|
||||
"created": 1763989808,
|
||||
"created": 1765495266,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "toolu_01VxSzmLcPK7UNNE4ms8XRa6",
|
||||
"id": "toolu_01CDkt9pjh8oC9C1984uhvuG",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
@@ -43,21 +43,23 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_253b4067-0c08-4807-8b36-23b576f138b5",
|
||||
"id": "msg_d7b3db5e-0402-42a8-937c-2ccf68a3e4e0",
|
||||
"role": "user",
|
||||
"created": 1763989808,
|
||||
"created": 1765495266,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolResponse",
|
||||
"id": "toolu_01VxSzmLcPK7UNNE4ms8XRa6",
|
||||
"id": "toolu_01CDkt9pjh8oC9C1984uhvuG",
|
||||
"toolResult": {
|
||||
"status": "success",
|
||||
"value": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
"value": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -305,11 +307,11 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989811,
|
||||
"created": 1765495269,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is currently **cloudy** with a temperature of **18°C** (64°F)."
|
||||
"text": "# Weather in Berlin, Germany\n\nThe current weather in Berlin, Germany is:\n- **Conditions**: Cloudy\n- **Temperature**: 18°C (64°F)\n\nIt's a mild day with overcast conditions in Berlin!"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -321,301 +323,8 @@
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"usage": {
|
||||
"input_tokens": 2406,
|
||||
"output_tokens": 29,
|
||||
"total_tokens": 2435
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1bc400a528c54b25f4f1f609481e98e44222b3deaf7eee2c9e640e6345c73861": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_8_0",
|
||||
"role": "user",
|
||||
"created": 1763989805,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "tell me what the weather is in Berlin, Germany"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "dynamic_task__create_task",
|
||||
"description": "Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, activities. Arrays for multiple tasks.",
|
||||
"inputSchema": {
|
||||
"$defs": {
|
||||
"TaskParameter": {
|
||||
"description": "Parameters for a single task",
|
||||
"properties": {
|
||||
"activities": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"extensions": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parameters": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"retry": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"return_last_only": {
|
||||
"description": "If true, return only the last message from the subagent (default: false, returns full conversation)",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"description": "How to execute multiple tasks (default: parallel for multiple tasks, sequential for single task)",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"task_parameters": {
|
||||
"description": "Array of tasks. Each task must have either 'instructions' OR 'prompt' field (at least one is required).",
|
||||
"items": {
|
||||
"$ref": "#/$defs/TaskParameter"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_parameters"
|
||||
],
|
||||
"title": "CreateDynamicTaskParams",
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Create Dynamic Tasks",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": false,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "platform__manage_schedule",
|
||||
"description": "Manage scheduled recipe execution for this goose instance.\n\nActions:\n- \"list\": List all scheduled jobs\n- \"create\": Create a new scheduled job from a recipe file\n- \"run_now\": Execute a scheduled job immediately \n- \"pause\": Pause a scheduled job\n- \"unpause\": Resume a paused job\n- \"delete\": Remove a scheduled job\n- \"kill\": Terminate a currently running job\n- \"inspect\": Get details about a running job\n- \"sessions\": List execution history for a job\n- \"session_content\": Get the full content (messages) of a specific session\n",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"list",
|
||||
"create",
|
||||
"run_now",
|
||||
"pause",
|
||||
"unpause",
|
||||
"delete",
|
||||
"kill",
|
||||
"inspect",
|
||||
"sessions",
|
||||
"session_content"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"cron_expression": {
|
||||
"description": "A cron expression for create action. Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats. 5-field expressions are automatically converted to 6-field by prepending '0' for seconds.",
|
||||
"type": "string"
|
||||
},
|
||||
"job_id": {
|
||||
"description": "Job identifier for operations on existing jobs",
|
||||
"type": "string"
|
||||
},
|
||||
"limit": {
|
||||
"default": 50,
|
||||
"description": "Limit for sessions list",
|
||||
"type": "integer"
|
||||
},
|
||||
"recipe_path": {
|
||||
"description": "Path to recipe file for create action",
|
||||
"type": "string"
|
||||
},
|
||||
"session_id": {
|
||||
"description": "Session identifier for session_content action",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Manage scheduled recipes",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "subagent__execute_task",
|
||||
"description": "Only use the subagent__execute_task tool when you execute sub recipe task or dynamic task.\n EXECUTION STRATEGY DECISION:\n 1. If the tasks are created with execution_mode, use the execution_mode.\n 2. Execute tasks sequentially unless user explicitly requests parallel execution. PARALLEL: User uses keywords like 'parallel', 'simultaneously', 'at the same time', 'concurrently'\n\n IMPLEMENTATION:\n - Sequential execution: Call this tool multiple times, passing exactly ONE task per call\n - Parallel execution: Call this tool once, passing an ARRAY of all tasks\n\n EXAMPLES:\n User Intent Based:\n - User: 'get weather and tell me a joke' → Sequential (2 separate tool calls, 1 task each)\n - User: 'get weather and joke in parallel' → Parallel (1 tool call with array of 2 tasks)\n - User: 'run these simultaneously' → Parallel (1 tool call with task array)\n - User: 'do task A then task B' → Sequential (2 separate tool calls)",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"default": "sequential",
|
||||
"description": "Execution strategy for multiple tasks. Use 'sequential' (default) unless user explicitly requests parallel execution with words like 'parallel', 'simultaneously', 'at the same time', or 'concurrently'.",
|
||||
"enum": [
|
||||
"sequential",
|
||||
"parallel"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"task_ids": {
|
||||
"items": {
|
||||
"description": "Unique identifier for the task",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_ids"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Run tasks in parallel",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "weather_extension__get_weather",
|
||||
"description": "Get the weather for a location",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"location": {
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989808,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "I'll get the current weather information for Berlin, Germany."
|
||||
},
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "toolu_01VxSzmLcPK7UNNE4ms8XRa6",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
"name": "weather_extension__get_weather",
|
||||
"arguments": {
|
||||
"location": "Berlin, Germany"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"usage": {
|
||||
"input_tokens": 2320,
|
||||
"output_tokens": 72,
|
||||
"total_tokens": 2392
|
||||
"output_tokens": 55,
|
||||
"total_tokens": 2461
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -627,7 +336,7 @@
|
||||
{
|
||||
"id": null,
|
||||
"role": "user",
|
||||
"created": 1763989805,
|
||||
"created": 1765495263,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -646,7 +355,7 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989810,
|
||||
"created": 1765495265,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -667,5 +376,298 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1bc400a528c54b25f4f1f609481e98e44222b3deaf7eee2c9e640e6345c73861": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251211_27_0",
|
||||
"role": "user",
|
||||
"created": 1765495263,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "tell me what the weather is in Berlin, Germany"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "dynamic_task__create_task",
|
||||
"description": "Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, activities. Arrays for multiple tasks.",
|
||||
"inputSchema": {
|
||||
"$defs": {
|
||||
"TaskParameter": {
|
||||
"description": "Parameters for a single task",
|
||||
"properties": {
|
||||
"activities": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"extensions": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parameters": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"retry": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"return_last_only": {
|
||||
"description": "If true, return only the last message from the subagent (default: false, returns full conversation)",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"description": "How to execute multiple tasks (default: parallel for multiple tasks, sequential for single task)",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"task_parameters": {
|
||||
"description": "Array of tasks. Each task must have either 'instructions' OR 'prompt' field (at least one is required).",
|
||||
"items": {
|
||||
"$ref": "#/$defs/TaskParameter"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_parameters"
|
||||
],
|
||||
"title": "CreateDynamicTaskParams",
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Create Dynamic Tasks",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": false,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "platform__manage_schedule",
|
||||
"description": "Manage scheduled recipe execution for this goose instance.\n\nActions:\n- \"list\": List all scheduled jobs\n- \"create\": Create a new scheduled job from a recipe file\n- \"run_now\": Execute a scheduled job immediately \n- \"pause\": Pause a scheduled job\n- \"unpause\": Resume a paused job\n- \"delete\": Remove a scheduled job\n- \"kill\": Terminate a currently running job\n- \"inspect\": Get details about a running job\n- \"sessions\": List execution history for a job\n- \"session_content\": Get the full content (messages) of a specific session\n",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"list",
|
||||
"create",
|
||||
"run_now",
|
||||
"pause",
|
||||
"unpause",
|
||||
"delete",
|
||||
"kill",
|
||||
"inspect",
|
||||
"sessions",
|
||||
"session_content"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"cron_expression": {
|
||||
"description": "A cron expression for create action. Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats. 5-field expressions are automatically converted to 6-field by prepending '0' for seconds.",
|
||||
"type": "string"
|
||||
},
|
||||
"job_id": {
|
||||
"description": "Job identifier for operations on existing jobs",
|
||||
"type": "string"
|
||||
},
|
||||
"limit": {
|
||||
"default": 50,
|
||||
"description": "Limit for sessions list",
|
||||
"type": "integer"
|
||||
},
|
||||
"recipe_path": {
|
||||
"description": "Path to recipe file for create action",
|
||||
"type": "string"
|
||||
},
|
||||
"session_id": {
|
||||
"description": "Session identifier for session_content action",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Manage scheduled recipes",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "subagent__execute_task",
|
||||
"description": "Only use the subagent__execute_task tool when you execute sub recipe task or dynamic task.\n EXECUTION STRATEGY DECISION:\n 1. If the tasks are created with execution_mode, use the execution_mode.\n 2. Execute tasks sequentially unless user explicitly requests parallel execution. PARALLEL: User uses keywords like 'parallel', 'simultaneously', 'at the same time', 'concurrently'\n\n IMPLEMENTATION:\n - Sequential execution: Call this tool multiple times, passing exactly ONE task per call\n - Parallel execution: Call this tool once, passing an ARRAY of all tasks\n\n EXAMPLES:\n User Intent Based:\n - User: 'get weather and tell me a joke' → Sequential (2 separate tool calls, 1 task each)\n - User: 'get weather and joke in parallel' → Parallel (1 tool call with array of 2 tasks)\n - User: 'run these simultaneously' → Parallel (1 tool call with task array)\n - User: 'do task A then task B' → Sequential (2 separate tool calls)",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"default": "sequential",
|
||||
"description": "Execution strategy for multiple tasks. Use 'sequential' (default) unless user explicitly requests parallel execution with words like 'parallel', 'simultaneously', 'at the same time', or 'concurrently'.",
|
||||
"enum": [
|
||||
"sequential",
|
||||
"parallel"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"task_ids": {
|
||||
"items": {
|
||||
"description": "Unique identifier for the task",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_ids"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Run tasks in parallel",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "weather_extension__get_weather",
|
||||
"description": "Get the weather for a location",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"location": {
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1765495266,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "I'll get the current weather information for Berlin, Germany for you."
|
||||
},
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "toolu_01CDkt9pjh8oC9C1984uhvuG",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
"name": "weather_extension__get_weather",
|
||||
"arguments": {
|
||||
"location": "Berlin, Germany"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"usage": {
|
||||
"input_tokens": 2320,
|
||||
"output_tokens": 74,
|
||||
"total_tokens": 2394
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,60 @@
|
||||
{
|
||||
"8b1b8633232ca390cb3ff37a48daf74168b58f13e2943efcb17957129ee34d1a": {
|
||||
"input": {
|
||||
"system": "Reply with only a description in four words or less",
|
||||
"messages": [
|
||||
{
|
||||
"id": null,
|
||||
"role": "user",
|
||||
"created": 1765495269,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here are the first few user messages:\ntell me what the weather is in Berlin, Germany\n\nBased on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": []
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1765495270,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Weather inquiry for Berlin"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "gpt-4o-mini-2024-07-18",
|
||||
"usage": {
|
||||
"input_tokens": 84,
|
||||
"output_tokens": 5,
|
||||
"total_tokens": 89
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1bc400a528c54b25f4f1f609481e98e44222b3deaf7eee2c9e640e6345c73861": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_9_0",
|
||||
"id": "msg_20251211_28_0",
|
||||
"role": "user",
|
||||
"created": 1763989812,
|
||||
"created": 1765495269,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -257,11 +305,11 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989813,
|
||||
"created": 1765495270,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "call_5IgyNY33nTougqsNFhizypew",
|
||||
"id": "call_JJok3ApyTL54ohjLX8tTYVHC",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
@@ -288,14 +336,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"2c07b9a6570269fef4ae329889ce5a5243d7b09b52165580be701b67b8b95818": {
|
||||
"e020c157caa13bad743162d7079a04ddafca2d075469124a0162c5562a2cf21b": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_9_0",
|
||||
"id": "msg_20251211_28_0",
|
||||
"role": "user",
|
||||
"created": 1763989812,
|
||||
"created": 1765495269,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -308,13 +356,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_b87436fe-d710-4fcd-b912-a69ab4ccfe7e",
|
||||
"id": "msg_1d08355f-6d2d-413e-a3b5-787d635f9c6e",
|
||||
"role": "assistant",
|
||||
"created": 1763989813,
|
||||
"created": 1765495270,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "call_5IgyNY33nTougqsNFhizypew",
|
||||
"id": "call_JJok3ApyTL54ohjLX8tTYVHC",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
@@ -332,21 +380,23 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_9cf859cf-4adc-4464-aba4-1d4ccf9d7d05",
|
||||
"id": "msg_929b0769-72dd-46f8-9027-31d1c09d4f08",
|
||||
"role": "user",
|
||||
"created": 1763989813,
|
||||
"created": 1765495270,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolResponse",
|
||||
"id": "call_5IgyNY33nTougqsNFhizypew",
|
||||
"id": "call_JJok3ApyTL54ohjLX8tTYVHC",
|
||||
"toolResult": {
|
||||
"status": "success",
|
||||
"value": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
"value": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -594,11 +644,11 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989814,
|
||||
"created": 1765495271,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "### Current Weather in Berlin, Germany\n\n- **Condition:** Cloudy\n- **Temperature:** 18°C"
|
||||
"text": "### Current Weather in Berlin, Germany\n\n- **Condition:** Cloudy\n- **Temperature:** 18°C\n\nIf you need more information or a forecast, let me know!"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -610,56 +660,8 @@
|
||||
"model": "gpt-4o-mini-2024-07-18",
|
||||
"usage": {
|
||||
"input_tokens": 1534,
|
||||
"output_tokens": 24,
|
||||
"total_tokens": 1558
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"8b1b8633232ca390cb3ff37a48daf74168b58f13e2943efcb17957129ee34d1a": {
|
||||
"input": {
|
||||
"system": "Reply with only a description in four words or less",
|
||||
"messages": [
|
||||
{
|
||||
"id": null,
|
||||
"role": "user",
|
||||
"created": 1763989812,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here are the first few user messages:\ntell me what the weather is in Berlin, Germany\n\nBased on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": []
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989813,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Berlin weather inquiry"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "gpt-4o-mini-2024-07-18",
|
||||
"usage": {
|
||||
"input_tokens": 84,
|
||||
"output_tokens": 4,
|
||||
"total_tokens": 88
|
||||
"output_tokens": 38,
|
||||
"total_tokens": 1572
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,301 +1,12 @@
|
||||
{
|
||||
"1bc400a528c54b25f4f1f609481e98e44222b3deaf7eee2c9e640e6345c73861": {
|
||||
"075d1e65f3a25bbdfc21aee19a1119b43fd4241f8643e646c9eb928a431975d4": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_10_0",
|
||||
"id": "msg_20251211_40_0",
|
||||
"role": "user",
|
||||
"created": 1763989814,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "tell me what the weather is in Berlin, Germany"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "dynamic_task__create_task",
|
||||
"description": "Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, activities. Arrays for multiple tasks.",
|
||||
"inputSchema": {
|
||||
"$defs": {
|
||||
"TaskParameter": {
|
||||
"description": "Parameters for a single task",
|
||||
"properties": {
|
||||
"activities": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"extensions": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parameters": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"retry": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"return_last_only": {
|
||||
"description": "If true, return only the last message from the subagent (default: false, returns full conversation)",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"description": "How to execute multiple tasks (default: parallel for multiple tasks, sequential for single task)",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"task_parameters": {
|
||||
"description": "Array of tasks. Each task must have either 'instructions' OR 'prompt' field (at least one is required).",
|
||||
"items": {
|
||||
"$ref": "#/$defs/TaskParameter"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_parameters"
|
||||
],
|
||||
"title": "CreateDynamicTaskParams",
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Create Dynamic Tasks",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": false,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "platform__manage_schedule",
|
||||
"description": "Manage scheduled recipe execution for this goose instance.\n\nActions:\n- \"list\": List all scheduled jobs\n- \"create\": Create a new scheduled job from a recipe file\n- \"run_now\": Execute a scheduled job immediately \n- \"pause\": Pause a scheduled job\n- \"unpause\": Resume a paused job\n- \"delete\": Remove a scheduled job\n- \"kill\": Terminate a currently running job\n- \"inspect\": Get details about a running job\n- \"sessions\": List execution history for a job\n- \"session_content\": Get the full content (messages) of a specific session\n",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"list",
|
||||
"create",
|
||||
"run_now",
|
||||
"pause",
|
||||
"unpause",
|
||||
"delete",
|
||||
"kill",
|
||||
"inspect",
|
||||
"sessions",
|
||||
"session_content"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"cron_expression": {
|
||||
"description": "A cron expression for create action. Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats. 5-field expressions are automatically converted to 6-field by prepending '0' for seconds.",
|
||||
"type": "string"
|
||||
},
|
||||
"job_id": {
|
||||
"description": "Job identifier for operations on existing jobs",
|
||||
"type": "string"
|
||||
},
|
||||
"limit": {
|
||||
"default": 50,
|
||||
"description": "Limit for sessions list",
|
||||
"type": "integer"
|
||||
},
|
||||
"recipe_path": {
|
||||
"description": "Path to recipe file for create action",
|
||||
"type": "string"
|
||||
},
|
||||
"session_id": {
|
||||
"description": "Session identifier for session_content action",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Manage scheduled recipes",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "subagent__execute_task",
|
||||
"description": "Only use the subagent__execute_task tool when you execute sub recipe task or dynamic task.\n EXECUTION STRATEGY DECISION:\n 1. If the tasks are created with execution_mode, use the execution_mode.\n 2. Execute tasks sequentially unless user explicitly requests parallel execution. PARALLEL: User uses keywords like 'parallel', 'simultaneously', 'at the same time', 'concurrently'\n\n IMPLEMENTATION:\n - Sequential execution: Call this tool multiple times, passing exactly ONE task per call\n - Parallel execution: Call this tool once, passing an ARRAY of all tasks\n\n EXAMPLES:\n User Intent Based:\n - User: 'get weather and tell me a joke' → Sequential (2 separate tool calls, 1 task each)\n - User: 'get weather and joke in parallel' → Parallel (1 tool call with array of 2 tasks)\n - User: 'run these simultaneously' → Parallel (1 tool call with task array)\n - User: 'do task A then task B' → Sequential (2 separate tool calls)",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"default": "sequential",
|
||||
"description": "Execution strategy for multiple tasks. Use 'sequential' (default) unless user explicitly requests parallel execution with words like 'parallel', 'simultaneously', 'at the same time', or 'concurrently'.",
|
||||
"enum": [
|
||||
"sequential",
|
||||
"parallel"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"task_ids": {
|
||||
"items": {
|
||||
"description": "Unique identifier for the task",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_ids"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Run tasks in parallel",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "weather_extension__get_weather",
|
||||
"description": "Get the weather for a location",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"location": {
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989814,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "4kpg6v08d",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
"name": "weather_extension__get_weather",
|
||||
"arguments": {
|
||||
"location": "Berlin, Germany"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "llama-3.3-70b-versatile",
|
||||
"usage": {
|
||||
"input_tokens": 2258,
|
||||
"output_tokens": 20,
|
||||
"total_tokens": 2278
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"165258e98e56cdcccdc49bd70ecd2b081b41d35ddb3559ed3bc7b1e420290bed": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_10_0",
|
||||
"role": "user",
|
||||
"created": 1763989814,
|
||||
"created": 1765495334,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -308,13 +19,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_9d25f6e9-6d02-4f41-8710-325795e87fbc",
|
||||
"id": "msg_6147fd68-5a38-4528-9924-d4dbaf5e7f44",
|
||||
"role": "assistant",
|
||||
"created": 1763989814,
|
||||
"created": 1765495335,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "4kpg6v08d",
|
||||
"id": "zemx6n94z",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
@@ -332,21 +43,23 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_513af900-f4fe-4ed0-aed8-7ffb4c8c384d",
|
||||
"id": "msg_0fac763c-62c3-4a33-a15b-27d605c5b81d",
|
||||
"role": "user",
|
||||
"created": 1763989814,
|
||||
"created": 1765495335,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolResponse",
|
||||
"id": "4kpg6v08d",
|
||||
"id": "zemx6n94z",
|
||||
"toolResult": {
|
||||
"status": "success",
|
||||
"value": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
"value": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -594,7 +307,7 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989815,
|
||||
"created": 1765495335,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -623,7 +336,7 @@
|
||||
{
|
||||
"id": null,
|
||||
"role": "user",
|
||||
"created": 1763989814,
|
||||
"created": 1765495334,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -642,11 +355,11 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989814,
|
||||
"created": 1765495334,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Berlin weather query"
|
||||
"text": "Berlin weather"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -658,8 +371,297 @@
|
||||
"model": "llama-3.3-70b-versatile",
|
||||
"usage": {
|
||||
"input_tokens": 108,
|
||||
"output_tokens": 4,
|
||||
"total_tokens": 112
|
||||
"output_tokens": 3,
|
||||
"total_tokens": 111
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1bc400a528c54b25f4f1f609481e98e44222b3deaf7eee2c9e640e6345c73861": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251211_40_0",
|
||||
"role": "user",
|
||||
"created": 1765495334,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "tell me what the weather is in Berlin, Germany"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "dynamic_task__create_task",
|
||||
"description": "Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, activities. Arrays for multiple tasks.",
|
||||
"inputSchema": {
|
||||
"$defs": {
|
||||
"TaskParameter": {
|
||||
"description": "Parameters for a single task",
|
||||
"properties": {
|
||||
"activities": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"extensions": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parameters": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"retry": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"return_last_only": {
|
||||
"description": "If true, return only the last message from the subagent (default: false, returns full conversation)",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"additionalProperties": true,
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"description": "How to execute multiple tasks (default: parallel for multiple tasks, sequential for single task)",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"task_parameters": {
|
||||
"description": "Array of tasks. Each task must have either 'instructions' OR 'prompt' field (at least one is required).",
|
||||
"items": {
|
||||
"$ref": "#/$defs/TaskParameter"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_parameters"
|
||||
],
|
||||
"title": "CreateDynamicTaskParams",
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Create Dynamic Tasks",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": false,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "platform__manage_schedule",
|
||||
"description": "Manage scheduled recipe execution for this goose instance.\n\nActions:\n- \"list\": List all scheduled jobs\n- \"create\": Create a new scheduled job from a recipe file\n- \"run_now\": Execute a scheduled job immediately \n- \"pause\": Pause a scheduled job\n- \"unpause\": Resume a paused job\n- \"delete\": Remove a scheduled job\n- \"kill\": Terminate a currently running job\n- \"inspect\": Get details about a running job\n- \"sessions\": List execution history for a job\n- \"session_content\": Get the full content (messages) of a specific session\n",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"list",
|
||||
"create",
|
||||
"run_now",
|
||||
"pause",
|
||||
"unpause",
|
||||
"delete",
|
||||
"kill",
|
||||
"inspect",
|
||||
"sessions",
|
||||
"session_content"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"cron_expression": {
|
||||
"description": "A cron expression for create action. Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats. 5-field expressions are automatically converted to 6-field by prepending '0' for seconds.",
|
||||
"type": "string"
|
||||
},
|
||||
"job_id": {
|
||||
"description": "Job identifier for operations on existing jobs",
|
||||
"type": "string"
|
||||
},
|
||||
"limit": {
|
||||
"default": 50,
|
||||
"description": "Limit for sessions list",
|
||||
"type": "integer"
|
||||
},
|
||||
"recipe_path": {
|
||||
"description": "Path to recipe file for create action",
|
||||
"type": "string"
|
||||
},
|
||||
"session_id": {
|
||||
"description": "Session identifier for session_content action",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Manage scheduled recipes",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "subagent__execute_task",
|
||||
"description": "Only use the subagent__execute_task tool when you execute sub recipe task or dynamic task.\n EXECUTION STRATEGY DECISION:\n 1. If the tasks are created with execution_mode, use the execution_mode.\n 2. Execute tasks sequentially unless user explicitly requests parallel execution. PARALLEL: User uses keywords like 'parallel', 'simultaneously', 'at the same time', 'concurrently'\n\n IMPLEMENTATION:\n - Sequential execution: Call this tool multiple times, passing exactly ONE task per call\n - Parallel execution: Call this tool once, passing an ARRAY of all tasks\n\n EXAMPLES:\n User Intent Based:\n - User: 'get weather and tell me a joke' → Sequential (2 separate tool calls, 1 task each)\n - User: 'get weather and joke in parallel' → Parallel (1 tool call with array of 2 tasks)\n - User: 'run these simultaneously' → Parallel (1 tool call with task array)\n - User: 'do task A then task B' → Sequential (2 separate tool calls)",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"execution_mode": {
|
||||
"default": "sequential",
|
||||
"description": "Execution strategy for multiple tasks. Use 'sequential' (default) unless user explicitly requests parallel execution with words like 'parallel', 'simultaneously', 'at the same time', or 'concurrently'.",
|
||||
"enum": [
|
||||
"sequential",
|
||||
"parallel"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"task_ids": {
|
||||
"items": {
|
||||
"description": "Unique identifier for the task",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_ids"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"annotations": {
|
||||
"title": "Run tasks in parallel",
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": true,
|
||||
"idempotentHint": false,
|
||||
"openWorldHint": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "weather_extension__get_weather",
|
||||
"description": "Get the weather for a location",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"location": {
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1765495335,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "zemx6n94z",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
"name": "weather_extension__get_weather",
|
||||
"arguments": {
|
||||
"location": "Berlin, Germany"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "llama-3.3-70b-versatile",
|
||||
"usage": {
|
||||
"input_tokens": 2258,
|
||||
"output_tokens": 20,
|
||||
"total_tokens": 2278
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_7_0",
|
||||
"id": "msg_20251211_26_0",
|
||||
"role": "user",
|
||||
"created": 1763989801,
|
||||
"created": 1765495261,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -257,11 +257,11 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989803,
|
||||
"created": 1765495262,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "call_GmIN2vOnjDg6MSVWWFuZ4FlV",
|
||||
"id": "call_sVLij6GavUkUc5Ri4VeDdgmN",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
@@ -288,62 +288,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"8b1b8633232ca390cb3ff37a48daf74168b58f13e2943efcb17957129ee34d1a": {
|
||||
"input": {
|
||||
"system": "Reply with only a description in four words or less",
|
||||
"messages": [
|
||||
{
|
||||
"id": null,
|
||||
"role": "user",
|
||||
"created": 1763989801,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here are the first few user messages:\ntell me what the weather is in Berlin, Germany\n\nBased on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": []
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989804,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Berlin weather inquiry"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "gpt-4o-2024-08-06",
|
||||
"usage": {
|
||||
"input_tokens": 84,
|
||||
"output_tokens": 3,
|
||||
"total_tokens": 87
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"98726e7698e93241dfcba182bdca3a7d0640f0f538f086f4981dca6689b7ab5a": {
|
||||
"950a6b5ec929abdd9e7cd337d2ea80c07631c23c6d64edac31988e18c6207e92": {
|
||||
"input": {
|
||||
"system": "You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.\ngoose is being developed as an open-source software project.\n\ngoose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,\nclaude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).\nThese models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10\nmonths prior to the current date.\n\n# Extensions\n\nExtensions allow other applications to provide context to goose. Extensions connect goose to different data sources and\ntools.\nYou are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level\nproblems using the tools in these extensions, and can interact with multiple at once.\n\nIf the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional\nextensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the\nextension_name. You should only enable extensions found from the search_available_extensions tool.\nIf Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load\nnew ones.\n\nBecause you dynamically load extensions, your conversation history may refer\nto interactions with extensions that are not currently active. The currently\nactive extensions are below. Each of these extensions provides tools that are\nin your tool specification.\n\n\n## weather_extension\n\n\n\n\n\n\n# Response Guidelines\n\n- Use Markdown formatting for all responses.\n- Follow best practices for Markdown, including:\n - Using headers for organization.\n - Bullet points for lists.\n - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic\n links using angle brackets (e.g., <http://example.com/>).\n- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the\n language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.\n- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_20251124_7_0",
|
||||
"id": "msg_20251211_26_0",
|
||||
"role": "user",
|
||||
"created": 1763989801,
|
||||
"created": 1765495261,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -356,13 +308,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_9a0fb3d7-d681-40b9-ab58-bb1cbe03e4b0",
|
||||
"id": "msg_899ef38b-1440-47f7-a942-1b2e58c8835c",
|
||||
"role": "assistant",
|
||||
"created": 1763989803,
|
||||
"created": 1765495262,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "call_GmIN2vOnjDg6MSVWWFuZ4FlV",
|
||||
"id": "call_sVLij6GavUkUc5Ri4VeDdgmN",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
@@ -380,21 +332,23 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "msg_51ae1986-b095-4163-8d42-004ff1a28c59",
|
||||
"id": "msg_2f8ddf2e-dedb-4b99-9549-826796292622",
|
||||
"role": "user",
|
||||
"created": 1763989803,
|
||||
"created": 1765495262,
|
||||
"content": [
|
||||
{
|
||||
"type": "toolResponse",
|
||||
"id": "call_GmIN2vOnjDg6MSVWWFuZ4FlV",
|
||||
"id": "call_sVLij6GavUkUc5Ri4VeDdgmN",
|
||||
"toolResult": {
|
||||
"status": "success",
|
||||
"value": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
"value": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in Berlin, Germany is cloudy and 18°C"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -642,7 +596,7 @@
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1763989805,
|
||||
"created": 1765495263,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -663,5 +617,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"8b1b8633232ca390cb3ff37a48daf74168b58f13e2943efcb17957129ee34d1a": {
|
||||
"input": {
|
||||
"system": "Reply with only a description in four words or less",
|
||||
"messages": [
|
||||
{
|
||||
"id": null,
|
||||
"role": "user",
|
||||
"created": 1765495261,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here are the first few user messages:\ntell me what the weather is in Berlin, Germany\n\nBased on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": []
|
||||
},
|
||||
"output": {
|
||||
"message": {
|
||||
"id": null,
|
||||
"role": "assistant",
|
||||
"created": 1765495262,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Berlin weather inquiry"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"userVisible": true,
|
||||
"agentVisible": true
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"model": "gpt-4o-2024-08-06",
|
||||
"usage": {
|
||||
"input_tokens": 84,
|
||||
"output_tokens": 3,
|
||||
"total_tokens": 87
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,6 +248,8 @@ pub struct SessionSettings {
|
||||
}
|
||||
|
||||
pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
goose::posthog::set_session_context("cli", session_config.resume);
|
||||
|
||||
let config = Config::global();
|
||||
|
||||
let (saved_provider, saved_model_config) = if session_config.resume {
|
||||
|
||||
@@ -221,12 +221,12 @@ pub fn tool_response_to_markdown(resp: &ToolResponse, export_all_content: bool)
|
||||
md.push_str("#### Tool Response:\n");
|
||||
|
||||
match &resp.tool_result {
|
||||
Ok(contents) => {
|
||||
if contents.is_empty() {
|
||||
Ok(result) => {
|
||||
if result.content.is_empty() {
|
||||
md.push_str("*No textual output from tool.*\n");
|
||||
}
|
||||
|
||||
for content in contents {
|
||||
for content in &result.content {
|
||||
if !export_all_content {
|
||||
if let Some(audience) = content.audience() {
|
||||
if !audience.contains(&Role::Assistant) {
|
||||
@@ -581,7 +581,12 @@ mod tests {
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "test-id".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let result = tool_response_to_markdown(&tool_response, true);
|
||||
@@ -601,7 +606,12 @@ mod tests {
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "test-id".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let result = tool_response_to_markdown(&tool_response, true);
|
||||
@@ -707,7 +717,12 @@ if __name__ == "__main__":
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "shell-cat".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let request_result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -748,7 +763,12 @@ if __name__ == "__main__":
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "git-status".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let request_result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -797,7 +817,12 @@ warning: unused variable `x`
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "cargo-build".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let response_result = tool_response_to_markdown(&tool_response, true);
|
||||
@@ -844,7 +869,12 @@ warning: unused variable `x`
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "curl-api".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let response_result = tool_response_to_markdown(&tool_response, true);
|
||||
@@ -880,7 +910,12 @@ warning: unused variable `x`
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "editor-write".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let request_result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -937,7 +972,12 @@ def process_data(data: List[Dict]) -> List[Dict]:
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "editor-view".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let response_result = tool_response_to_markdown(&tool_response, true);
|
||||
@@ -974,7 +1014,12 @@ Command failed with exit code 2"#;
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "shell-error".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let response_result = tool_response_to_markdown(&tool_response, true);
|
||||
@@ -1014,7 +1059,12 @@ Command failed with exit code 2"#;
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "script-exec".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let request_result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -1061,7 +1111,12 @@ drwx------ 3 user staff 96 Dec 6 16:20 com.apple.launchd.abc
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "multi-cmd".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let request_result = tool_request_to_markdown(&_tool_request, true);
|
||||
@@ -1104,7 +1159,12 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "grep-search".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let request_result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -1144,7 +1204,12 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "json-test".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let response_result = tool_response_to_markdown(&tool_response, true);
|
||||
@@ -1185,7 +1250,12 @@ found 0 vulnerabilities"#;
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
id: "npm-install".to_string(),
|
||||
tool_result: Ok(vec![Content::text(text_content.raw.text)]),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let request_result = tool_request_to_markdown(&tool_request, true);
|
||||
|
||||
@@ -285,8 +285,8 @@ fn render_tool_response(resp: &ToolResponse, theme: Theme, debug: bool) {
|
||||
let config = Config::global();
|
||||
|
||||
match &resp.tool_result {
|
||||
Ok(contents) => {
|
||||
for content in contents {
|
||||
Ok(result) => {
|
||||
for content in &result.content {
|
||||
if let Some(audience) = content.audience() {
|
||||
if !audience.contains(&rmcp::model::Role::User) {
|
||||
continue;
|
||||
|
||||
@@ -39,12 +39,13 @@ reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "m
|
||||
tokio-util = "0.7.15"
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
serde_path_to_error = "0.1.20"
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] }
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
|
||||
url = "2.5.7"
|
||||
rand = "0.9.2"
|
||||
hex = "0.4.3"
|
||||
socket2 = "0.6.1"
|
||||
fs2 = "0.4.3"
|
||||
rustls = { version = "0.23", features = ["ring"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { version = "0.55.0" }
|
||||
|
||||
@@ -354,6 +354,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::start_agent,
|
||||
super::routes::agent::resume_agent,
|
||||
super::routes::agent::get_tools,
|
||||
super::routes::agent::read_resource,
|
||||
super::routes::agent::call_tool,
|
||||
super::routes::agent::update_from_session,
|
||||
super::routes::agent::agent_add_extension,
|
||||
super::routes::agent::agent_remove_extension,
|
||||
@@ -514,6 +516,10 @@ derive_utoipa!(Icon as IconSchema);
|
||||
goose::agents::types::SuccessCheck,
|
||||
super::routes::agent::UpdateProviderRequest,
|
||||
super::routes::agent::GetToolsQuery,
|
||||
super::routes::agent::ReadResourceRequest,
|
||||
super::routes::agent::ReadResourceResponse,
|
||||
super::routes::agent::CallToolRequest,
|
||||
super::routes::agent::CallToolResponse,
|
||||
super::routes::agent::UpdateRouterToolSelectorRequest,
|
||||
super::routes::agent::StartAgentRequest,
|
||||
super::routes::agent::ResumeAgentRequest,
|
||||
|
||||
@@ -32,6 +32,7 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
@@ -91,6 +92,18 @@ pub struct RemoveExtensionRequest {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct ReadResourceRequest {
|
||||
session_id: String,
|
||||
extension_name: String,
|
||||
uri: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ReadResourceResponse {
|
||||
html: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/start",
|
||||
@@ -106,6 +119,8 @@ async fn start_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<StartAgentRequest>,
|
||||
) -> Result<Json<Session>, ErrorResponse> {
|
||||
goose::posthog::set_session_context("desktop", false);
|
||||
|
||||
let StartAgentRequest {
|
||||
working_dir,
|
||||
recipe,
|
||||
@@ -198,6 +213,8 @@ async fn resume_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<ResumeAgentRequest>,
|
||||
) -> Result<Json<Session>, ErrorResponse> {
|
||||
goose::posthog::set_session_context("desktop", true);
|
||||
|
||||
let session = SessionManager::get_session(&payload.session_id, true)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -481,7 +498,7 @@ async fn update_router_tool_selector(
|
||||
.update_router_tool_selector(None, Some(true))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to update tool selection strategy: {}", e);
|
||||
error!("Failed to update tool selection strategy: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
@@ -575,6 +592,39 @@ pub struct CallToolResponse {
|
||||
pub is_error: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/read_resource",
|
||||
request_body = ReadResourceRequest,
|
||||
responses(
|
||||
(status = 200, description = "Resource read successfully", body = ReadResourceResponse),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
(status = 404, description = "Resource not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
async fn read_resource(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<ReadResourceRequest>,
|
||||
) -> Result<Json<ReadResourceResponse>, StatusCode> {
|
||||
let agent = state
|
||||
.get_agent_for_route(payload.session_id.clone())
|
||||
.await?;
|
||||
|
||||
let html = agent
|
||||
.extension_manager
|
||||
.read_ui_resource(
|
||||
&payload.uri,
|
||||
&payload.extension_name,
|
||||
CancellationToken::default(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ReadResourceResponse { html }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/call_tool",
|
||||
@@ -620,15 +670,16 @@ async fn call_tool(
|
||||
Ok(tool_result) => {
|
||||
let content_result = tool_result.result.await;
|
||||
match content_result {
|
||||
Ok(content) => {
|
||||
let output = content
|
||||
Ok(call_tool_result) => {
|
||||
let output = call_tool_result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|c| c.as_text().map(|t| t.text.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Ok(Json(CallToolResponse {
|
||||
output,
|
||||
is_error: false,
|
||||
is_error: call_tool_result.is_error.unwrap_or(false),
|
||||
}))
|
||||
}
|
||||
Err(error_data) => Ok(Json(CallToolResponse {
|
||||
@@ -649,6 +700,8 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/agent/start", post(start_agent))
|
||||
.route("/agent/resume", post(resume_agent))
|
||||
.route("/agent/tools", get(get_tools))
|
||||
.route("/agent/read_resource", post(read_resource))
|
||||
.route("/agent/call_tool", post(call_tool))
|
||||
.route("/agent/update_provider", post(update_agent_provider))
|
||||
.route(
|
||||
"/agent/update_router_tool_selector",
|
||||
@@ -658,6 +711,5 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/agent/add_extension", post(agent_add_extension))
|
||||
.route("/agent/remove_extension", post(agent_remove_extension))
|
||||
.route("/agent/stop", post(stop_agent))
|
||||
.route("/agent/call_tool", post(call_tool))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -392,7 +392,9 @@ pub async fn get_provider_models(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
match provider.fetch_supported_models().await {
|
||||
let models_result = provider.fetch_recommended_models().await;
|
||||
|
||||
match models_result {
|
||||
Ok(Some(models)) => Ok(Json(models)),
|
||||
Ok(None) => Ok(Json(Vec::new())),
|
||||
Err(provider_error) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use goose::config::ConfigError;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
@@ -37,3 +38,9 @@ impl From<anyhow::Error> for ErrorResponse {
|
||||
Self::internal(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for ErrorResponse {
|
||||
fn from(err: ConfigError) -> Self {
|
||||
Self::internal(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,6 +477,8 @@ async fn run_single_connection(
|
||||
server_secret: String,
|
||||
restart_tx: mpsc::Sender<()>,
|
||||
) {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let worker_url = get_worker_url();
|
||||
let ws_url = worker_url
|
||||
.replace("https://", "wss://")
|
||||
|
||||
@@ -165,8 +165,8 @@ impl ChatComponent {
|
||||
&tool_name, &tool_args, width, color, items, map, msg_idx,
|
||||
);
|
||||
|
||||
if let Ok(contents) = &resp.tool_result {
|
||||
Self::render_tool_response_body(contents, width, items, map, msg_idx);
|
||||
if let Ok(call_tool_result) = &resp.tool_result {
|
||||
Self::render_tool_response_body(&call_tool_result.content, width, items, map, msg_idx);
|
||||
}
|
||||
|
||||
let footer = format!("╰{:─<width$}╯", "", width = width.saturating_sub(2));
|
||||
|
||||
@@ -108,10 +108,10 @@ impl MessagePopup {
|
||||
.fg(theme.status.warning)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
let Ok(contents) = &resp.tool_result else {
|
||||
let Ok(call_tool_result) = &resp.tool_result else {
|
||||
return;
|
||||
};
|
||||
for content in contents {
|
||||
for content in &call_tool_result.content {
|
||||
if let Some(audience) = content.audience() {
|
||||
if !audience.contains(&rmcp::model::Role::User) {
|
||||
continue;
|
||||
|
||||
@@ -123,8 +123,8 @@ fn handle_event(
|
||||
println!("[tool_result] {status}");
|
||||
|
||||
match &resp.tool_result {
|
||||
Ok(contents) => {
|
||||
for content in contents {
|
||||
Ok(call_tool_result) => {
|
||||
for content in &call_tool_result.content {
|
||||
if let Some(audience) = content.audience() {
|
||||
if !audience.contains(&rmcp::model::Role::User) {
|
||||
continue;
|
||||
|
||||
@@ -21,8 +21,8 @@ pub fn message_to_plain_text(message: &Message) -> String {
|
||||
}
|
||||
MessageContent::ToolResponse(resp) => {
|
||||
lines.push("Tool Output:".to_string());
|
||||
if let Ok(contents) = &resp.tool_result {
|
||||
for content in contents {
|
||||
if let Ok(call_tool_result) = &resp.tool_result {
|
||||
for content in &call_tool_result.content {
|
||||
if let Some(audience) = content.audience() {
|
||||
if !audience.contains(&rmcp::model::Role::User) {
|
||||
continue;
|
||||
|
||||
@@ -111,6 +111,7 @@ shellexpand = "3.1.1"
|
||||
indexmap = "2.12.0"
|
||||
ignore = "0.4.25"
|
||||
which = "8.0.0"
|
||||
posthog-rs = "0.3.7"
|
||||
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,254 @@
|
||||
/// Build canonical models from OpenRouter API
|
||||
///
|
||||
/// This script fetches models from OpenRouter and converts them to canonical format.
|
||||
/// Usage:
|
||||
/// cargo run --example build_canonical_models
|
||||
///
|
||||
use anyhow::{Context, Result};
|
||||
use goose::providers::canonical::{
|
||||
canonical_name, CanonicalModel, CanonicalModelRegistry, Pricing,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const OPENROUTER_API_URL: &str = "https://openrouter.ai/api/v1/models";
|
||||
const ALLOWED_PROVIDERS: &[&str] = &[
|
||||
"anthropic",
|
||||
"google",
|
||||
"openai",
|
||||
"meta-llama",
|
||||
"mistralai",
|
||||
"x-ai",
|
||||
"deepseek",
|
||||
"cohere",
|
||||
"ai21",
|
||||
"qwen",
|
||||
];
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
println!("Fetching models from OpenRouter API...");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(OPENROUTER_API_URL)
|
||||
.header("User-Agent", "goose/canonical-builder")
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to fetch from OpenRouter API")?;
|
||||
|
||||
let json: Value = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse OpenRouter response")?;
|
||||
|
||||
let models = json["data"]
|
||||
.as_array()
|
||||
.context("Expected 'data' array in OpenRouter response")?
|
||||
.clone();
|
||||
|
||||
println!("Processing {} models from OpenRouter...", models.len());
|
||||
|
||||
// First pass: Group models by canonical ID and track the one with shortest name
|
||||
let mut canonical_groups: HashMap<String, &Value> = HashMap::new();
|
||||
let mut shortest_names: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for model in &models {
|
||||
let id = model["id"].as_str().unwrap();
|
||||
let name = model["name"].as_str().context("Model missing id field")?;
|
||||
|
||||
// Skip OpenRouter-specific pricing variants (:free, :nitro)
|
||||
// Keep :extended since it has different context length
|
||||
if id.contains(":free") || id.contains(":nitro") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let canonical_id = canonical_name("openrouter", id);
|
||||
|
||||
let provider = canonical_id.split('/').next().unwrap_or("");
|
||||
if !ALLOWED_PROVIDERS.contains(&provider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prompt_cost = model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("prompt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let completion_cost = model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("completion"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let has_paid_pricing = prompt_cost > 0.0 || completion_cost > 0.0;
|
||||
|
||||
if let Some(existing_model) = canonical_groups.get(&canonical_id) {
|
||||
let existing_name = shortest_names.get(&canonical_id).unwrap();
|
||||
|
||||
let existing_prompt = existing_model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("prompt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let existing_completion = existing_model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("completion"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let existing_has_paid = existing_prompt > 0.0 || existing_completion > 0.0;
|
||||
|
||||
let should_replace = if has_paid_pricing != existing_has_paid {
|
||||
has_paid_pricing // Prefer the one with paid pricing
|
||||
} else {
|
||||
name.len() < existing_name.len() // Both same pricing tier, prefer shorter name
|
||||
};
|
||||
|
||||
if should_replace {
|
||||
println!(
|
||||
" Updating {} from '{}' (paid: {}) to '{}' (paid: {})",
|
||||
canonical_id,
|
||||
existing_model["id"].as_str().unwrap(),
|
||||
existing_has_paid,
|
||||
id,
|
||||
has_paid_pricing
|
||||
);
|
||||
shortest_names.insert(canonical_id.clone(), name.to_string());
|
||||
canonical_groups.insert(canonical_id, model);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
" Adding: {} (from {}, paid: {})",
|
||||
canonical_id, id, has_paid_pricing
|
||||
);
|
||||
shortest_names.insert(canonical_id.clone(), name.to_string());
|
||||
canonical_groups.insert(canonical_id, model);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out beta/preview variants if non-beta version exists
|
||||
let beta_suffixes = ["-beta", "-preview", "-alpha"];
|
||||
let mut to_remove = Vec::new();
|
||||
|
||||
for canonical_id in canonical_groups.keys() {
|
||||
for suffix in &beta_suffixes {
|
||||
if canonical_id.ends_with(suffix) {
|
||||
// Check if non-beta version exists
|
||||
let base_id = canonical_id.strip_suffix(suffix).unwrap();
|
||||
if canonical_groups.contains_key(base_id) {
|
||||
println!(
|
||||
" Filtering out {} (non-beta version {} exists)",
|
||||
canonical_id, base_id
|
||||
);
|
||||
to_remove.push(canonical_id.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id in to_remove {
|
||||
canonical_groups.remove(&id);
|
||||
shortest_names.remove(&id);
|
||||
}
|
||||
|
||||
// Second pass: Build the registry with the selected models
|
||||
let mut registry = CanonicalModelRegistry::new();
|
||||
|
||||
for (canonical_id, model) in canonical_groups.iter() {
|
||||
let name = shortest_names.get(canonical_id).unwrap();
|
||||
|
||||
let context_length = model["context_length"].as_u64().unwrap_or(128_000) as usize;
|
||||
|
||||
let max_completion_tokens = model
|
||||
.get("top_provider")
|
||||
.and_then(|tp| tp.get("max_completion_tokens"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as usize);
|
||||
|
||||
let input_modalities: Vec<String> = model
|
||||
.get("architecture")
|
||||
.and_then(|arch| arch.get("input_modalities"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec!["text".to_string()]);
|
||||
|
||||
let output_modalities: Vec<String> = model
|
||||
.get("architecture")
|
||||
.and_then(|arch| arch.get("output_modalities"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec!["text".to_string()]);
|
||||
|
||||
let supports_tools = model
|
||||
.get("supported_parameters")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|params| params.iter().any(|param| param.as_str() == Some("tools")))
|
||||
.unwrap_or(false);
|
||||
|
||||
let pricing_obj = model
|
||||
.get("pricing")
|
||||
.context("Model missing pricing field")?;
|
||||
let pricing = Pricing {
|
||||
prompt: pricing_obj
|
||||
.get("prompt")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
completion: pricing_obj
|
||||
.get("completion")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
request: pricing_obj
|
||||
.get("request")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
image: pricing_obj
|
||||
.get("image")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
};
|
||||
|
||||
let canonical_model = CanonicalModel {
|
||||
id: canonical_id.clone(),
|
||||
name: name.to_string(),
|
||||
context_length,
|
||||
max_completion_tokens,
|
||||
input_modalities,
|
||||
output_modalities,
|
||||
supports_tools,
|
||||
pricing,
|
||||
};
|
||||
|
||||
registry.register(canonical_model);
|
||||
}
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
let output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/providers/canonical/data/canonical_models.json");
|
||||
registry.to_file(&output_path)?;
|
||||
println!(
|
||||
"\n✓ Wrote {} models to {}",
|
||||
registry.count(),
|
||||
output_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/// Canonical Model Checker
|
||||
///
|
||||
/// This script checks which models from top providers are properly mapped to canonical models.
|
||||
/// It maintains a lock file of mappings and detects changes between runs.
|
||||
///
|
||||
/// Outputs:
|
||||
/// - Models that are NOT mapped to canonical models
|
||||
/// - Full list of (provider, model) <-> canonical-model mappings
|
||||
/// - Diff report showing mapping changes since last run:
|
||||
/// * Changed mappings (model now maps to a different canonical model)
|
||||
/// * Added mappings (model gained a canonical mapping)
|
||||
/// * Removed mappings (model lost its canonical mapping)
|
||||
///
|
||||
/// Output File:
|
||||
/// - src/providers/canonical/data/canonical_mapping_report.json
|
||||
/// Contains full report with mapping data (acts as a lock file)
|
||||
///
|
||||
/// Usage:
|
||||
/// cargo run --example canonical_model_checker -- [--output custom_path.json]
|
||||
///
|
||||
use anyhow::{Context, Result};
|
||||
use goose::providers::{canonical::ModelMapping, create_with_named_model};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
struct ProviderModelPair {
|
||||
provider: String,
|
||||
model: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct MappingEntry {
|
||||
provider: String,
|
||||
model: String,
|
||||
canonical: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct MappingReport {
|
||||
/// Timestamp of this report
|
||||
timestamp: String,
|
||||
|
||||
/// Models that are NOT mapped to canonical models
|
||||
unmapped_models: Vec<ProviderModelPair>,
|
||||
|
||||
/// All mappings: (provider, model) -> canonical model
|
||||
/// Stored per provider for backward compatibility
|
||||
all_mappings: HashMap<String, Vec<ModelMapping>>,
|
||||
|
||||
/// Flat list of all mappings for easier comparison (lock file format)
|
||||
mapped_models: Vec<MappingEntry>,
|
||||
|
||||
/// Total models checked per provider
|
||||
model_counts: HashMap<String, usize>,
|
||||
|
||||
/// Canonical models referenced
|
||||
canonical_models_used: HashSet<String>,
|
||||
}
|
||||
|
||||
impl MappingReport {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||
unmapped_models: Vec::new(),
|
||||
all_mappings: HashMap::new(),
|
||||
mapped_models: Vec::new(),
|
||||
model_counts: HashMap::new(),
|
||||
canonical_models_used: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_provider_results(
|
||||
&mut self,
|
||||
provider_name: &str,
|
||||
fetched_models: Vec<String>,
|
||||
mappings: Vec<ModelMapping>,
|
||||
) {
|
||||
let mapping_map: HashMap<String, String> = mappings
|
||||
.iter()
|
||||
.map(|m| (m.provider_model.clone(), m.canonical_model.clone()))
|
||||
.collect();
|
||||
|
||||
for model in &fetched_models {
|
||||
if !mapping_map.contains_key(model) {
|
||||
self.unmapped_models.push(ProviderModelPair {
|
||||
provider: provider_name.to_string(),
|
||||
model: model.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (model, canonical) in &mapping_map {
|
||||
self.canonical_models_used.insert(canonical.clone());
|
||||
self.mapped_models.push(MappingEntry {
|
||||
provider: provider_name.to_string(),
|
||||
model: model.clone(),
|
||||
canonical: canonical.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
self.all_mappings
|
||||
.insert(provider_name.to_string(), mappings);
|
||||
self.model_counts
|
||||
.insert(provider_name.to_string(), fetched_models.len());
|
||||
}
|
||||
|
||||
fn print_summary(&self) {
|
||||
println!("\n{}", "=".repeat(80));
|
||||
println!("CANONICAL MODEL MAPPING REPORT");
|
||||
println!("{}", "=".repeat(80));
|
||||
println!("\nGenerated: {}\n", self.timestamp);
|
||||
|
||||
println!("Models Checked Per Provider:");
|
||||
println!("{}", "-".repeat(80));
|
||||
let mut providers: Vec<_> = self.model_counts.iter().collect();
|
||||
providers.sort_by_key(|(name, _)| *name);
|
||||
for (provider, count) in providers {
|
||||
let mapped = self
|
||||
.all_mappings
|
||||
.get(provider)
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
let unmapped = count - mapped;
|
||||
println!(
|
||||
" {:<20} Total: {:>3} Mapped: {:>3} Unmapped: {:>3}",
|
||||
provider, count, mapped, unmapped
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n{}", "=".repeat(80));
|
||||
println!("UNMAPPED MODELS ({})", self.unmapped_models.len());
|
||||
println!("{}", "=".repeat(80));
|
||||
|
||||
if self.unmapped_models.is_empty() {
|
||||
println!("✓ All models are mapped to canonical models!");
|
||||
} else {
|
||||
let mut unmapped_by_provider: HashMap<&str, Vec<&str>> = HashMap::new();
|
||||
for pair in &self.unmapped_models {
|
||||
unmapped_by_provider
|
||||
.entry(pair.provider.as_str())
|
||||
.or_default()
|
||||
.push(pair.model.as_str());
|
||||
}
|
||||
|
||||
let mut providers: Vec<_> = unmapped_by_provider.keys().collect();
|
||||
providers.sort();
|
||||
|
||||
for provider in providers {
|
||||
println!("\n{}:", provider);
|
||||
let mut models = unmapped_by_provider[provider].to_vec();
|
||||
models.sort();
|
||||
for model in models {
|
||||
println!(" - {}", model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "=".repeat(80));
|
||||
println!(
|
||||
"CANONICAL MODELS REFERENCED ({})",
|
||||
self.canonical_models_used.len()
|
||||
);
|
||||
println!("{}", "=".repeat(80));
|
||||
if self.canonical_models_used.is_empty() {
|
||||
println!(" (none yet)");
|
||||
} else {
|
||||
let mut canonical: Vec<_> = self.canonical_models_used.iter().collect();
|
||||
canonical.sort();
|
||||
for model in canonical {
|
||||
println!(" - {}", model);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "=".repeat(80));
|
||||
}
|
||||
|
||||
fn compare_with_previous(&self, previous: &MappingReport) {
|
||||
println!("\n{}", "=".repeat(80));
|
||||
println!("CHANGES SINCE PREVIOUS RUN");
|
||||
println!("{}", "=".repeat(80));
|
||||
|
||||
let mut prev_map: HashMap<(String, String), String> = HashMap::new();
|
||||
for entry in &previous.mapped_models {
|
||||
prev_map.insert(
|
||||
(entry.provider.clone(), entry.model.clone()),
|
||||
entry.canonical.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut curr_map: HashMap<(String, String), String> = HashMap::new();
|
||||
for entry in &self.mapped_models {
|
||||
curr_map.insert(
|
||||
(entry.provider.clone(), entry.model.clone()),
|
||||
entry.canonical.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut changed_mappings = Vec::new();
|
||||
let mut added_mappings = Vec::new();
|
||||
let mut removed_mappings = Vec::new();
|
||||
|
||||
for (key @ (provider, model), canonical) in &curr_map {
|
||||
match prev_map.get(key) {
|
||||
Some(prev_canonical) if prev_canonical != canonical => {
|
||||
changed_mappings.push((
|
||||
provider.clone(),
|
||||
model.clone(),
|
||||
prev_canonical.clone(),
|
||||
canonical.clone(),
|
||||
));
|
||||
}
|
||||
None => {
|
||||
added_mappings.push((provider.clone(), model.clone(), canonical.clone()));
|
||||
}
|
||||
_ => {
|
||||
// No change
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (key @ (provider, model), canonical) in &prev_map {
|
||||
if !curr_map.contains_key(key) {
|
||||
removed_mappings.push((provider.clone(), model.clone(), canonical.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if changed_mappings.is_empty() && added_mappings.is_empty() && removed_mappings.is_empty() {
|
||||
println!("\nNo changes in model mappings.");
|
||||
} else {
|
||||
if !changed_mappings.is_empty() {
|
||||
println!("\n⚠ Changed Mappings ({}):", changed_mappings.len());
|
||||
println!(" (Models that now map to a different canonical model)");
|
||||
for (provider, model, old_canonical, new_canonical) in changed_mappings {
|
||||
println!(" {} / {}", provider, model);
|
||||
println!(" WAS: {}", old_canonical);
|
||||
println!(" NOW: {}", new_canonical);
|
||||
}
|
||||
}
|
||||
|
||||
if !added_mappings.is_empty() {
|
||||
println!("\n✓ Added Mappings ({}):", added_mappings.len());
|
||||
println!(" (Models that gained a canonical mapping)");
|
||||
for (provider, model, canonical) in added_mappings {
|
||||
println!(" {} / {} -> {}", provider, model, canonical);
|
||||
}
|
||||
}
|
||||
|
||||
if !removed_mappings.is_empty() {
|
||||
println!("\n✗ Removed Mappings ({}):", removed_mappings.len());
|
||||
println!(" (Models that lost their canonical mapping)");
|
||||
for (provider, model, canonical) in removed_mappings {
|
||||
println!(" {} / {} (was: {})", provider, model, canonical);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "=".repeat(80));
|
||||
}
|
||||
|
||||
fn save_to_file(&self, path: &PathBuf) -> Result<()> {
|
||||
let json = serde_json::to_string_pretty(self).context("Failed to serialize report")?;
|
||||
std::fs::write(path, json).context("Failed to write report file")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_from_file(path: &PathBuf) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path).context("Failed to read report file")?;
|
||||
let report: MappingReport =
|
||||
serde_json::from_str(&content).context("Failed to parse report file")?;
|
||||
Ok(report)
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_provider(
|
||||
provider_name: &str,
|
||||
model_for_init: &str,
|
||||
) -> Result<(Vec<String>, Vec<ModelMapping>)> {
|
||||
println!("Checking provider: {}", provider_name);
|
||||
|
||||
let provider = match create_with_named_model(provider_name, model_for_init).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
println!(" ⚠ Failed to create provider: {}", e);
|
||||
println!(" This is expected if credentials are not configured.");
|
||||
return Ok((Vec::new(), Vec::new()));
|
||||
}
|
||||
};
|
||||
|
||||
let fetched_models = match provider.fetch_supported_models().await {
|
||||
Ok(Some(models)) => {
|
||||
println!(" ✓ Fetched {} models", models.len());
|
||||
models
|
||||
}
|
||||
Ok(None) => {
|
||||
println!(" ⚠ Provider does not support model listing");
|
||||
Vec::new()
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ⚠ Failed to fetch models: {}", e);
|
||||
println!(" This is expected if credentials are not configured.");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let mut mappings = Vec::new();
|
||||
for model in &fetched_models {
|
||||
match provider.map_to_canonical_model(model).await {
|
||||
Ok(Some(canonical)) => {
|
||||
mappings.push(ModelMapping::new(model.clone(), canonical));
|
||||
}
|
||||
Ok(None) => {
|
||||
// No mapping found for this model
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ⚠ Failed to map model '{}': {}", model, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" ✓ Found {} mappings", mappings.len());
|
||||
|
||||
Ok((fetched_models, mappings))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
println!("Canonical Model Checker");
|
||||
println!("Checking model mappings for top providers...\n");
|
||||
|
||||
// Define providers to check with their default models
|
||||
let providers = vec![
|
||||
("anthropic", "claude-3-5-sonnet-20241022"),
|
||||
("openai", "gpt-4"),
|
||||
("openrouter", "anthropic/claude-3.5-sonnet"),
|
||||
("google", "gemini-1.5-pro-002"),
|
||||
("tetrate", "claude-3-5-sonnet-computer-use"),
|
||||
("xai", "grok-code-fast-1"),
|
||||
];
|
||||
|
||||
let mut report = MappingReport::new();
|
||||
|
||||
for (provider_name, default_model) in providers {
|
||||
let (fetched, mappings) = check_provider(provider_name, default_model).await?;
|
||||
report.add_provider_results(provider_name, fetched, mappings);
|
||||
println!();
|
||||
}
|
||||
|
||||
report.print_summary();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let output_path = if args.len() > 2 && args[1] == "--output" {
|
||||
PathBuf::from(&args[2])
|
||||
} else {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/providers/canonical/data/canonical_mapping_report.json")
|
||||
};
|
||||
|
||||
if output_path.exists() {
|
||||
if let Ok(previous) = MappingReport::load_from_file(&output_path) {
|
||||
report.compare_with_previous(&previous);
|
||||
}
|
||||
}
|
||||
|
||||
report.save_to_file(&output_path)?;
|
||||
println!("\n✓ Report saved to: {}", output_path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -37,8 +37,15 @@ async fn main() -> Result<()> {
|
||||
arguments: Some(object!({"path": "./test_image.png"})),
|
||||
}),
|
||||
),
|
||||
Message::user()
|
||||
.with_tool_response("000", Ok(vec![Content::image(base64_image, "image/png")])),
|
||||
Message::user().with_tool_response(
|
||||
"000",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::image(base64_image, "image/png")],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
// Get a response from the model about the image
|
||||
|
||||
@@ -58,7 +58,7 @@ use crate::tool_monitor::RepetitionInspector;
|
||||
use crate::utils::is_token_cancelled;
|
||||
use regex::Regex;
|
||||
use rmcp::model::{
|
||||
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt,
|
||||
CallToolRequestParam, CallToolResult, Content, ErrorCode, ErrorData, GetPromptResult, Prompt,
|
||||
ServerNotification, Tool,
|
||||
};
|
||||
use serde_json::Value;
|
||||
@@ -100,7 +100,7 @@ pub struct Agent {
|
||||
pub(super) prompt_manager: Mutex<PromptManager>,
|
||||
pub(super) confirmation_tx: mpsc::Sender<(String, PermissionConfirmation)>,
|
||||
pub(super) confirmation_rx: Mutex<mpsc::Receiver<(String, PermissionConfirmation)>>,
|
||||
pub(super) tool_result_tx: mpsc::Sender<(String, ToolResult<Vec<Content>>)>,
|
||||
pub(super) tool_result_tx: mpsc::Sender<(String, ToolResult<CallToolResult>)>,
|
||||
pub(super) tool_result_rx: ToolResultReceiver,
|
||||
|
||||
pub tool_route_manager: Arc<ToolRouteManager>,
|
||||
@@ -128,7 +128,8 @@ pub enum ToolStreamItem<T> {
|
||||
Result(T),
|
||||
}
|
||||
|
||||
pub type ToolStream = Pin<Box<dyn Stream<Item = ToolStreamItem<ToolResult<Vec<Content>>>> + Send>>;
|
||||
pub type ToolStream =
|
||||
Pin<Box<dyn Stream<Item = ToolStreamItem<ToolResult<CallToolResult>>> + Send>>;
|
||||
|
||||
// tool_stream combines a stream of ServerNotifications with a future representing the
|
||||
// final result of the tool call. MCP notifications are not request-scoped, but
|
||||
@@ -137,7 +138,7 @@ pub type ToolStream = Pin<Box<dyn Stream<Item = ToolStreamItem<ToolResult<Vec<Co
|
||||
pub fn tool_stream<S, F>(rx: S, done: F) -> ToolStream
|
||||
where
|
||||
S: Stream<Item = ServerNotification> + Send + Unpin + 'static,
|
||||
F: Future<Output = ToolResult<Vec<Content>>> + Send + 'static,
|
||||
F: Future<Output = ToolResult<CallToolResult>> + Send + 'static,
|
||||
{
|
||||
Box::pin(async_stream::stream! {
|
||||
tokio::pin!(done);
|
||||
@@ -360,7 +361,12 @@ impl Agent {
|
||||
let mut response = response_msg.lock().await;
|
||||
*response = response.clone().with_tool_response(
|
||||
request.id.clone(),
|
||||
Ok(vec![rmcp::model::Content::text(DECLINED_RESPONSE)]),
|
||||
Ok(CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(DECLINED_RESPONSE)],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -454,7 +460,13 @@ impl Agent {
|
||||
let result = self
|
||||
.handle_schedule_management(arguments, request_id.clone())
|
||||
.await;
|
||||
return (request_id, Ok(ToolCallResult::from(result)));
|
||||
let wrapped_result = result.map(|content| CallToolResult {
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
});
|
||||
return (request_id, Ok(ToolCallResult::from(wrapped_result)));
|
||||
}
|
||||
|
||||
if tool_call.name == FINAL_OUTPUT_TOOL_NAME {
|
||||
@@ -1090,7 +1102,12 @@ impl Agent {
|
||||
let mut response = response_msg.lock().await;
|
||||
*response = response.clone().with_tool_response(
|
||||
request.id.clone(),
|
||||
Ok(vec![Content::text(CHAT_MODE_TOOL_SKIPPED_RESPONSE)]),
|
||||
Ok(CallToolResult {
|
||||
content: vec![Content::text(CHAT_MODE_TOOL_SKIPPED_RESPONSE)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1221,7 +1238,8 @@ impl Agent {
|
||||
no_tools_called = false;
|
||||
}
|
||||
}
|
||||
Err(ProviderError::ContextLengthExceeded(_error_msg)) => {
|
||||
Err(ref provider_err @ ProviderError::ContextLengthExceeded(_)) => {
|
||||
crate::posthog::emit_error(provider_err.telemetry_type());
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
@@ -1255,11 +1273,12 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error: {}", e);
|
||||
Err(ref provider_err) => {
|
||||
crate::posthog::emit_error(provider_err.telemetry_type());
|
||||
error!("Error: {}", provider_err);
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_text(
|
||||
format!("Ran into this error: {e}.\n\nPlease retry if you think this is a transient or recoverable error.")
|
||||
format!("Ran into this error: {provider_err}.\n\nPlease retry if you think this is a transient or recoverable error.")
|
||||
)
|
||||
);
|
||||
break;
|
||||
@@ -1419,7 +1438,7 @@ impl Agent {
|
||||
Ok(plan_prompt)
|
||||
}
|
||||
|
||||
pub async fn handle_tool_result(&self, id: String, result: ToolResult<Vec<Content>>) {
|
||||
pub async fn handle_tool_result(&self, id: String, result: ToolResult<CallToolResult>) {
|
||||
if let Err(e) = self.tool_result_tx.send((id, result)).await {
|
||||
error!("Failed to send tool result: {}", e);
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ use crate::oauth::oauth_flow;
|
||||
use crate::prompt_template;
|
||||
use crate::subprocess::configure_command_no_window;
|
||||
use rmcp::model::{
|
||||
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, ResourceContents,
|
||||
ServerInfo, Tool,
|
||||
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, RawContent,
|
||||
Resource, ResourceContents, ServerInfo, Tool,
|
||||
};
|
||||
use rmcp::transport::auth::AuthClient;
|
||||
use schemars::_private::NoSerialize;
|
||||
@@ -758,6 +758,7 @@ impl ExtensionManager {
|
||||
uri,
|
||||
extension_name.unwrap(),
|
||||
cancellation_token.clone(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
return Ok(result);
|
||||
@@ -773,7 +774,12 @@ impl ExtensionManager {
|
||||
|
||||
for extension_name in extension_names {
|
||||
let result = self
|
||||
.read_resource_from_extension(uri, &extension_name, cancellation_token.clone())
|
||||
.read_resource_from_extension(
|
||||
uri,
|
||||
&extension_name,
|
||||
cancellation_token.clone(),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
match result {
|
||||
Ok(result) => return Ok(result),
|
||||
@@ -807,6 +813,7 @@ impl ExtensionManager {
|
||||
uri: &str,
|
||||
extension_name: &str,
|
||||
cancellation_token: CancellationToken,
|
||||
format_with_uri: bool,
|
||||
) -> Result<Vec<Content>, ErrorData> {
|
||||
let available_extensions = self
|
||||
.extensions
|
||||
@@ -840,9 +847,12 @@ impl ExtensionManager {
|
||||
|
||||
let mut result = Vec::new();
|
||||
for content in read_result.contents {
|
||||
// Only reading the text resource content; skipping the blob content cause it's too long
|
||||
if let ResourceContents::TextResourceContents { text, .. } = content {
|
||||
let content_str = format!("{}\n\n{}", uri, text);
|
||||
let content_str = if format_with_uri {
|
||||
format!("{}\n\n{}", uri, text)
|
||||
} else {
|
||||
text
|
||||
};
|
||||
result.push(Content::text(content_str));
|
||||
}
|
||||
}
|
||||
@@ -850,6 +860,65 @@ impl ExtensionManager {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_ui_resources(&self) -> Result<Vec<(String, Resource)>, ErrorData> {
|
||||
let mut ui_resources = Vec::new();
|
||||
|
||||
let extensions_to_check: Vec<(String, McpClientBox)> = {
|
||||
let extensions = self.extensions.lock().await;
|
||||
extensions
|
||||
.iter()
|
||||
.map(|(name, ext)| (name.clone(), ext.get_client()))
|
||||
.collect()
|
||||
};
|
||||
|
||||
for (extension_name, client) in extensions_to_check {
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
match client_guard
|
||||
.list_resources(None, CancellationToken::default())
|
||||
.await
|
||||
{
|
||||
Ok(list_response) => {
|
||||
for resource in list_response.resources {
|
||||
if resource.uri.starts_with("ui://") {
|
||||
ui_resources.push((extension_name.clone(), resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to list resources for {}: {:?}", extension_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ui_resources)
|
||||
}
|
||||
|
||||
pub async fn read_ui_resource(
|
||||
&self,
|
||||
uri: &str,
|
||||
extension_name: &str,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<String, ErrorData> {
|
||||
let contents = self
|
||||
.read_resource_from_extension(uri, extension_name, cancellation_token, false)
|
||||
.await?;
|
||||
|
||||
contents
|
||||
.into_iter()
|
||||
.find_map(|c| match c.raw {
|
||||
RawContent::Text(text_content) => Some(text_content.text),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
ErrorData::new(
|
||||
ErrorCode::RESOURCE_NOT_FOUND,
|
||||
format!("No text content in resource '{}'", uri),
|
||||
None,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_resources_from_extension(
|
||||
&self,
|
||||
extension_name: &str,
|
||||
@@ -936,7 +1005,6 @@ impl ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Log any errors that occurred
|
||||
if !errors.is_empty() {
|
||||
tracing::error!(
|
||||
errors = ?errors
|
||||
@@ -998,7 +1066,6 @@ impl ExtensionManager {
|
||||
client_guard
|
||||
.call_tool(&tool_name, arguments, cancellation_token)
|
||||
.await
|
||||
.map(|call| call.content)
|
||||
.map_err(|e| match e {
|
||||
ServiceError::McpError(error_data) => error_data,
|
||||
_ => {
|
||||
@@ -1077,7 +1144,6 @@ impl ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Log any errors that occurred
|
||||
if !errors.is_empty() {
|
||||
tracing::debug!(
|
||||
errors = ?errors
|
||||
|
||||
@@ -123,9 +123,14 @@ impl FinalOutputTool {
|
||||
match result {
|
||||
Ok(parsed_value) => {
|
||||
self.final_output = Some(Self::parsed_final_output_string(parsed_value));
|
||||
ToolCallResult::from(Ok(vec![Content::text(
|
||||
"Final output successfully collected.".to_string(),
|
||||
)]))
|
||||
ToolCallResult::from(Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(
|
||||
"Final output successfully collected.".to_string(),
|
||||
)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
Err(error) => ToolCallResult::from(Err(ErrorData {
|
||||
code: ErrorCode::INVALID_PARAMS,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::Utc;
|
||||
use rmcp::model::{Content, ErrorData};
|
||||
use rmcp::model::{CallToolResult, Content, ErrorData};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
@@ -7,13 +7,13 @@ const LARGE_TEXT_THRESHOLD: usize = 200_000;
|
||||
|
||||
/// Process tool response and handle large text content
|
||||
pub fn process_tool_response(
|
||||
response: Result<Vec<Content>, ErrorData>,
|
||||
) -> Result<Vec<Content>, ErrorData> {
|
||||
response: Result<CallToolResult, ErrorData>,
|
||||
) -> Result<CallToolResult, ErrorData> {
|
||||
match response {
|
||||
Ok(contents) => {
|
||||
Ok(mut result) => {
|
||||
let mut processed_contents = Vec::new();
|
||||
|
||||
for content in contents {
|
||||
for content in result.content {
|
||||
match content.as_text() {
|
||||
Some(text_content) => {
|
||||
// Check if text exceeds threshold
|
||||
@@ -51,7 +51,8 @@ pub fn process_tool_response(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(processed_contents)
|
||||
result.content = processed_contents;
|
||||
Ok(result)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
@@ -89,14 +90,19 @@ mod tests {
|
||||
let small_text = "This is a small text response";
|
||||
let content = Content::text(small_text.to_string());
|
||||
|
||||
let response = Ok(vec![content]);
|
||||
let response = Ok(CallToolResult {
|
||||
content: vec![content],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
});
|
||||
|
||||
// Process the response
|
||||
let processed = process_tool_response(response).unwrap();
|
||||
|
||||
// Verify the response is unchanged
|
||||
assert_eq!(processed.len(), 1);
|
||||
if let Some(text_content) = processed[0].as_text() {
|
||||
assert_eq!(processed.content.len(), 1);
|
||||
if let Some(text_content) = processed.content[0].as_text() {
|
||||
assert_eq!(text_content.text, small_text);
|
||||
} else {
|
||||
panic!("Expected text content");
|
||||
@@ -109,14 +115,19 @@ mod tests {
|
||||
let large_text = "a".repeat(LARGE_TEXT_THRESHOLD + 1000);
|
||||
let content = Content::text(large_text.clone());
|
||||
|
||||
let response = Ok(vec![content]);
|
||||
let response = Ok(CallToolResult {
|
||||
content: vec![content],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
});
|
||||
|
||||
// Process the response
|
||||
let processed = process_tool_response(response).unwrap();
|
||||
|
||||
// Verify the response contains a message about the file
|
||||
assert_eq!(processed.len(), 1);
|
||||
if let Some(text_content) = processed[0].as_text() {
|
||||
assert_eq!(processed.content.len(), 1);
|
||||
if let Some(text_content) = processed.content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("The response returned from the tool call was larger"));
|
||||
@@ -146,14 +157,19 @@ mod tests {
|
||||
// Create an image content
|
||||
let image_content = Content::image("base64data".to_string(), "image/png".to_string());
|
||||
|
||||
let response = Ok(vec![image_content]);
|
||||
let response = Ok(CallToolResult {
|
||||
content: vec![image_content],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
});
|
||||
|
||||
// Process the response
|
||||
let processed = process_tool_response(response).unwrap();
|
||||
|
||||
// Verify the response is unchanged
|
||||
assert_eq!(processed.len(), 1);
|
||||
if let Some(img) = processed[0].as_image() {
|
||||
assert_eq!(processed.content.len(), 1);
|
||||
if let Some(img) = processed.content[0].as_image() {
|
||||
assert_eq!(img.data, "base64data");
|
||||
assert_eq!(img.mime_type, "image/png");
|
||||
} else {
|
||||
@@ -168,23 +184,28 @@ mod tests {
|
||||
let large_text = Content::text("a".repeat(LARGE_TEXT_THRESHOLD + 1000));
|
||||
let image = Content::image("image_data".to_string(), "image/jpeg".to_string());
|
||||
|
||||
let response = Ok(vec![small_text, large_text, image]);
|
||||
let response = Ok(CallToolResult {
|
||||
content: vec![small_text, large_text, image],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
});
|
||||
|
||||
// Process the response
|
||||
let processed = process_tool_response(response).unwrap();
|
||||
|
||||
// Verify each item is handled correctly
|
||||
assert_eq!(processed.len(), 3);
|
||||
assert_eq!(processed.content.len(), 3);
|
||||
|
||||
// First item should be unchanged small text
|
||||
if let Some(text_content) = processed[0].as_text() {
|
||||
if let Some(text_content) = processed.content[0].as_text() {
|
||||
assert_eq!(text_content.text, "Small text");
|
||||
} else {
|
||||
panic!("Expected text content");
|
||||
}
|
||||
|
||||
// Second item should be a message about the file
|
||||
if let Some(text_content) = processed[1].as_text() {
|
||||
if let Some(text_content) = processed.content[1].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("The response returned from the tool call was larger"));
|
||||
@@ -201,7 +222,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Third item should be unchanged image
|
||||
if let Some(img) = processed[2].as_image() {
|
||||
if let Some(img) = processed.content[2].as_image() {
|
||||
assert_eq!(img.data, "image_data");
|
||||
assert_eq!(img.mime_type, "image/jpeg");
|
||||
} else {
|
||||
@@ -217,7 +238,7 @@ mod tests {
|
||||
message: Cow::from("Test error"),
|
||||
data: None,
|
||||
};
|
||||
let response: Result<Vec<Content>, ErrorData> = Err(error);
|
||||
let response: Result<CallToolResult, ErrorData> = Err(error);
|
||||
|
||||
// Process the response
|
||||
let processed = process_tool_response(response);
|
||||
|
||||
@@ -106,7 +106,15 @@ mod tests {
|
||||
arguments: None,
|
||||
}),
|
||||
),
|
||||
Message::user().with_tool_response("search_1", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"search_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::assistant()
|
||||
.with_text("I need to search more")
|
||||
.with_tool_request(
|
||||
@@ -116,7 +124,15 @@ mod tests {
|
||||
arguments: None,
|
||||
}),
|
||||
),
|
||||
Message::user().with_tool_response("search_2", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"search_2",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
let result = inject_moim(conv, &em).await;
|
||||
|
||||
@@ -370,5 +370,10 @@ pub async fn create_dynamic_task(
|
||||
};
|
||||
|
||||
tasks_manager.save_tasks(tasks).await;
|
||||
ToolCallResult::from(Ok(vec![Content::text(tasks_json)]))
|
||||
ToolCallResult::from(Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(tasks_json)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -63,7 +63,12 @@ impl SubRecipeManager {
|
||||
.call_sub_recipe_tool(tool_name, params, tasks_manager, parent_working_dir)
|
||||
.await;
|
||||
match result {
|
||||
Ok(call_result) => ToolCallResult::from(Ok(call_result)),
|
||||
Ok(content) => ToolCallResult::from(Ok(rmcp::model::CallToolResult {
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})),
|
||||
Err(e) => ToolCallResult::from(Err(ErrorData {
|
||||
code: ErrorCode::INTERNAL_ERROR,
|
||||
message: Cow::from(e.to_string()),
|
||||
|
||||
@@ -82,7 +82,12 @@ pub async fn run_tasks(
|
||||
{
|
||||
Ok(result) => {
|
||||
let output = serde_json::to_string(&result).unwrap();
|
||||
Ok(vec![Content::text(output)])
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(output)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(ErrorData {
|
||||
code: ErrorCode::INTERNAL_ERROR,
|
||||
|
||||
@@ -61,8 +61,9 @@ pub async fn run_complete_subagent_task(
|
||||
tool_response,
|
||||
) => {
|
||||
// Extract text from tool response
|
||||
if let Ok(contents) = &tool_response.tool_result {
|
||||
let texts: Vec<String> = contents
|
||||
if let Ok(result) = &tool_response.tool_result {
|
||||
let texts: Vec<String> = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| {
|
||||
if let rmcp::model::RawContent::Text(raw_text_content) =
|
||||
|
||||
@@ -16,12 +16,12 @@ use rmcp::model::{Content, ServerNotification};
|
||||
// ToolCallResult combines the result of a tool call with an optional notification stream that
|
||||
// can be used to receive notifications from the tool.
|
||||
pub struct ToolCallResult {
|
||||
pub result: Box<dyn Future<Output = ToolResult<Vec<Content>>> + Send + Unpin>,
|
||||
pub result: Box<dyn Future<Output = ToolResult<rmcp::model::CallToolResult>> + Send + Unpin>,
|
||||
pub notification_stream: Option<Box<dyn Stream<Item = ServerNotification> + Send + Unpin>>,
|
||||
}
|
||||
|
||||
impl From<ToolResult<Vec<Content>>> for ToolCallResult {
|
||||
fn from(result: ToolResult<Vec<Content>>) -> Self {
|
||||
impl From<ToolResult<rmcp::model::CallToolResult>> for ToolCallResult {
|
||||
fn from(result: ToolResult<rmcp::model::CallToolResult>) -> Self {
|
||||
Self {
|
||||
result: Box::new(futures::future::ready(result)),
|
||||
notification_stream: None,
|
||||
@@ -122,7 +122,12 @@ impl Agent {
|
||||
let mut response = response_msg.lock().await;
|
||||
*response = response.clone().with_tool_response(
|
||||
request.id.clone(),
|
||||
Ok(vec![Content::text(DECLINED_RESPONSE)]),
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(DECLINED_RESPONSE)],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ impl ToolRouteManager {
|
||||
|
||||
pub async fn record_tool_requests(&self, requests: &[ToolRequest]) {
|
||||
let selector = self.router_tool_selector.lock().await.clone();
|
||||
if let Some(selector) = selector {
|
||||
for request in requests {
|
||||
if let Ok(tool_call) = &request.tool_call {
|
||||
for request in requests {
|
||||
if let Ok(tool_call) = &request.tool_call {
|
||||
if let Some(ref selector) = selector {
|
||||
if let Err(e) = selector.record_tool_call(&tool_call.name).await {
|
||||
error!("Failed to record tool call: {}", e);
|
||||
}
|
||||
@@ -56,7 +56,12 @@ impl ToolRouteManager {
|
||||
let selector = self.router_tool_selector.lock().await.clone();
|
||||
match selector.as_ref() {
|
||||
Some(selector) => match selector.select_tools(arguments).await {
|
||||
Ok(tools) => Ok(ToolCallResult::from(Ok(tools))),
|
||||
Ok(content) => Ok(ToolCallResult::from(Ok(rmcp::model::CallToolResult {
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}))),
|
||||
Err(e) => Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Failed to select tools: {}", e),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use crate::providers::base::Provider;
|
||||
use rmcp::model::{Content, Tool};
|
||||
use rmcp::model::{CallToolResult, Tool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Type alias for the tool result channel receiver
|
||||
pub type ToolResultReceiver = Arc<Mutex<mpsc::Receiver<(String, ToolResult<Vec<Content>>)>>>;
|
||||
pub type ToolResultReceiver = Arc<Mutex<mpsc::Receiver<(String, ToolResult<CallToolResult>)>>>;
|
||||
|
||||
// We use double Arc here to allow easy provider swaps while sharing concurrent access
|
||||
pub type SharedProvider = Arc<Mutex<Option<Arc<dyn Provider>>>>;
|
||||
|
||||
@@ -355,8 +355,9 @@ fn format_message_for_compacting(msg: &Message) -> String {
|
||||
}
|
||||
}
|
||||
MessageContent::ToolResponse(res) => {
|
||||
if let Ok(contents) = &res.tool_result {
|
||||
let text_items: Vec<String> = contents
|
||||
if let Ok(result) = &res.tool_result {
|
||||
let text_items: Vec<String> = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| {
|
||||
content.as_text().map(|text_str| text_str.text.clone())
|
||||
@@ -517,7 +518,12 @@ mod tests {
|
||||
),
|
||||
Message::user().with_tool_response(
|
||||
"tool_0",
|
||||
Ok(vec![RawContent::text("hello, world").no_annotation()]),
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![RawContent::text("hello, world").no_annotation()],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -550,9 +556,12 @@ mod tests {
|
||||
));
|
||||
messages.push(Message::user().with_tool_response(
|
||||
format!("tool_{}", i),
|
||||
Ok(vec![
|
||||
RawContent::text(format!("response{}", i)).no_annotation(),
|
||||
]),
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![RawContent::text(format!("response{}", i)).no_annotation()],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use chrono::Utc;
|
||||
use rmcp::model::{
|
||||
AnnotateAble, CallToolRequestParam, Content, ImageContent, JsonObject, PromptMessage,
|
||||
PromptMessageContent, PromptMessageRole, RawContent, RawImageContent, RawTextContent,
|
||||
ResourceContents, Role, TextContent,
|
||||
AnnotateAble, CallToolRequestParam, CallToolResult, Content, ImageContent, JsonObject,
|
||||
PromptMessage, PromptMessageContent, PromptMessageRole, RawContent, RawImageContent,
|
||||
RawTextContent, ResourceContents, Role, TextContent,
|
||||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashSet;
|
||||
@@ -86,9 +86,9 @@ impl ToolRequest {
|
||||
#[derive(ToSchema)]
|
||||
pub struct ToolResponse {
|
||||
pub id: String,
|
||||
#[serde(with = "tool_result_serde")]
|
||||
#[serde(with = "tool_result_serde::call_tool_result")]
|
||||
#[schema(value_type = Object)]
|
||||
pub tool_result: ToolResult<Vec<Content>>,
|
||||
pub tool_result: ToolResult<CallToolResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -190,7 +190,7 @@ impl fmt::Display for MessageContent {
|
||||
f,
|
||||
"[ToolResponse: {}]",
|
||||
match &r.tool_result {
|
||||
Ok(contents) => format!("{} content item(s)", contents.len()),
|
||||
Ok(result) => format!("{} content item(s)", result.content.len()),
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
),
|
||||
@@ -266,7 +266,7 @@ impl MessageContent {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tool_response<S: Into<String>>(id: S, tool_result: ToolResult<Vec<Content>>) -> Self {
|
||||
pub fn tool_response<S: Into<String>>(id: S, tool_result: ToolResult<CallToolResult>) -> Self {
|
||||
MessageContent::ToolResponse(ToolResponse {
|
||||
id: id.into(),
|
||||
tool_result,
|
||||
@@ -380,8 +380,9 @@ impl MessageContent {
|
||||
|
||||
pub fn as_tool_response_text(&self) -> Option<String> {
|
||||
if let Some(tool_response) = self.as_tool_response() {
|
||||
if let Ok(contents) = &tool_response.tool_result {
|
||||
let texts: Vec<String> = contents
|
||||
if let Ok(result) = &tool_response.tool_result {
|
||||
let texts: Vec<String> = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| content.as_text().map(|t| t.text.to_string()))
|
||||
.collect();
|
||||
@@ -644,7 +645,7 @@ impl Message {
|
||||
pub fn with_tool_response<S: Into<String>>(
|
||||
self,
|
||||
id: S,
|
||||
result: ToolResult<Vec<Content>>,
|
||||
result: ToolResult<CallToolResult>,
|
||||
) -> Self {
|
||||
self.with_content(MessageContent::tool_response(id, result))
|
||||
}
|
||||
@@ -1302,4 +1303,87 @@ mod tests {
|
||||
assert!(metadata.user_visible);
|
||||
assert!(metadata.agent_visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_tool_response_deserialization() {
|
||||
let legacy_json = r#"{
|
||||
"role": "user",
|
||||
"created": 1640995200,
|
||||
"content": [{
|
||||
"type": "toolResponse",
|
||||
"id": "tool123",
|
||||
"toolResult": {
|
||||
"status": "success",
|
||||
"value": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Tool output text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}],
|
||||
"metadata": { "agentVisible": true, "userVisible": true }
|
||||
}"#;
|
||||
|
||||
let message: Message = serde_json::from_str(legacy_json).unwrap();
|
||||
assert_eq!(message.content.len(), 1);
|
||||
|
||||
if let MessageContent::ToolResponse(response) = &message.content[0] {
|
||||
assert_eq!(response.id, "tool123");
|
||||
if let Ok(result) = &response.tool_result {
|
||||
assert_eq!(result.content.len(), 1);
|
||||
assert_eq!(
|
||||
result.content[0].as_text().unwrap().text,
|
||||
"Tool output text"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected successful tool result");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected ToolResponse content");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_tool_response_deserialization() {
|
||||
let new_json = r#"{
|
||||
"role": "user",
|
||||
"created": 1640995200,
|
||||
"content": [{
|
||||
"type": "toolResponse",
|
||||
"id": "tool456",
|
||||
"toolResult": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "New format output"
|
||||
}
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
}],
|
||||
"metadata": { "agentVisible": true, "userVisible": true }
|
||||
}"#;
|
||||
|
||||
let message: Message = serde_json::from_str(new_json).unwrap();
|
||||
assert_eq!(message.content.len(), 1);
|
||||
|
||||
if let MessageContent::ToolResponse(response) = &message.content[0] {
|
||||
assert_eq!(response.id, "tool456");
|
||||
if let Ok(result) = &response.tool_result {
|
||||
assert_eq!(result.content.len(), 1);
|
||||
assert_eq!(
|
||||
result.content[0].as_text().unwrap().text,
|
||||
"New format output"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected successful tool result");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected ToolResponse content");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +555,15 @@ mod tests {
|
||||
arguments: Some(object!({"query": "rust programming"})),
|
||||
}),
|
||||
),
|
||||
Message::user().with_tool_response("search_1", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"search_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::assistant().with_text("Based on the search results, here's what I found..."),
|
||||
];
|
||||
|
||||
@@ -592,7 +600,15 @@ mod tests {
|
||||
Message::user().with_text("Another user message"),
|
||||
Message::assistant()
|
||||
.with_text("Response")
|
||||
.with_tool_response("orphan_1", Ok(vec![])), // Wrong role
|
||||
.with_tool_response(
|
||||
"orphan_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
), // Wrong role
|
||||
Message::assistant().with_thinking("Let me think", "sig"),
|
||||
Message::user()
|
||||
.with_tool_request(
|
||||
@@ -642,7 +658,15 @@ mod tests {
|
||||
}),
|
||||
),
|
||||
Message::user(),
|
||||
Message::user().with_tool_response("wrong_id", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"wrong_id",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::assistant().with_tool_request(
|
||||
"search_2",
|
||||
Ok(CallToolRequestParam {
|
||||
@@ -687,7 +711,12 @@ mod tests {
|
||||
.with_tool_request("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(CallToolRequestParam { name: "developer__shell".into(), arguments: Some(object!({"command": "wc slack.yaml"})) })),
|
||||
|
||||
Message::user()
|
||||
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(vec![])),
|
||||
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})),
|
||||
|
||||
Message::assistant()
|
||||
.with_text("I ran `ls -la` in the current directory and found several files. Looking at the file sizes, I can see that both `slack.yaml` and `subrecipes.yaml` are 0 bytes (the smallest files). I ran a word count on `slack.yaml` which shows: **0 lines**, **0 words**, **0 characters**"),
|
||||
@@ -718,7 +747,15 @@ mod tests {
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
),
|
||||
Message::user().with_tool_response("search_1", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"search_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::user().with_text("Thanks!"),
|
||||
];
|
||||
|
||||
|
||||
@@ -25,13 +25,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// For deserialization, let's use a simpler approach that works with the format we're serializing to
|
||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<ToolResult<T>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Define a helper enum to handle the two possible formats
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ResultFormat<T> {
|
||||
@@ -68,3 +66,79 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod call_tool_result {
|
||||
use super::*;
|
||||
use rmcp::model::{CallToolResult, Content};
|
||||
|
||||
pub fn serialize<S>(
|
||||
value: &ToolResult<CallToolResult>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
super::serialize(value, serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<ToolResult<CallToolResult>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ResultFormat {
|
||||
NewSuccess {
|
||||
status: String,
|
||||
value: CallToolResult,
|
||||
},
|
||||
LegacySuccess {
|
||||
status: String,
|
||||
value: Vec<Content>,
|
||||
},
|
||||
Error {
|
||||
status: String,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
let format = ResultFormat::deserialize(deserializer)?;
|
||||
|
||||
match format {
|
||||
ResultFormat::NewSuccess { status, value } => {
|
||||
if status == "success" {
|
||||
Ok(Ok(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Expected status 'success', got '{}'",
|
||||
status
|
||||
)))
|
||||
}
|
||||
}
|
||||
ResultFormat::LegacySuccess { status, value } => {
|
||||
if status == "success" {
|
||||
Ok(Ok(CallToolResult::success(value)))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Expected status 'success', got '{}'",
|
||||
status
|
||||
)))
|
||||
}
|
||||
}
|
||||
ResultFormat::Error { status, error } => {
|
||||
if status == "error" {
|
||||
Ok(Err(ErrorData {
|
||||
code: ErrorCode::INTERNAL_ERROR,
|
||||
message: Cow::from(error),
|
||||
data: None,
|
||||
}))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Expected status 'error', got '{}'",
|
||||
status
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod mcp_utils;
|
||||
pub mod model;
|
||||
pub mod oauth;
|
||||
pub mod permission;
|
||||
pub mod posthog;
|
||||
pub mod prompt_template;
|
||||
pub mod providers;
|
||||
pub mod recipe;
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct ToolPermissionRecord {
|
||||
allowed: bool,
|
||||
context_hash: String, // Hash of the tool's arguments/context to differentiate similar calls
|
||||
#[serde(skip_serializing_if = "Option::is_none")] // Don't serialize if None
|
||||
readable_context: Option<String>, // Add this field
|
||||
readable_context: Option<String>,
|
||||
timestamp: i64,
|
||||
expiry: Option<i64>, // Optional expiry timestamp
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
//! PostHog telemetry - fires once per session creation.
|
||||
|
||||
use crate::config::paths::Paths;
|
||||
use crate::config::{get_enabled_extensions, Config};
|
||||
use crate::session::SessionManager;
|
||||
use chrono::{DateTime, Utc};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
const POSTHOG_API_KEY: &str = "phc_RyX5CaY01VtZJCQyhSR5KFh6qimUy81YwxsEpotAftT";
|
||||
|
||||
/// Config key for telemetry opt-out preference
|
||||
pub const TELEMETRY_ENABLED_KEY: &str = "GOOSE_TELEMETRY_ENABLED";
|
||||
|
||||
static TELEMETRY_DISABLED_BY_ENV: Lazy<AtomicBool> = Lazy::new(|| {
|
||||
std::env::var("GOOSE_TELEMETRY_OFF")
|
||||
.map(|v| v == "1" || v.to_lowercase() == "true")
|
||||
.unwrap_or(false)
|
||||
.into()
|
||||
});
|
||||
|
||||
/// Check if telemetry is enabled.
|
||||
///
|
||||
/// Returns false if:
|
||||
/// - GOOSE_TELEMETRY_OFF environment variable is set to "1" or "true"
|
||||
/// - GOOSE_TELEMETRY_ENABLED config value is set to false
|
||||
///
|
||||
/// Returns true otherwise (telemetry is opt-out, enabled by default)
|
||||
pub fn is_telemetry_enabled() -> bool {
|
||||
if TELEMETRY_DISABLED_BY_ENV.load(Ordering::Relaxed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let config = Config::global();
|
||||
config
|
||||
.get_param::<bool>(TELEMETRY_ENABLED_KEY)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Installation Tracking
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct InstallationData {
|
||||
installation_id: String,
|
||||
first_seen: DateTime<Utc>,
|
||||
session_count: u32,
|
||||
}
|
||||
|
||||
impl Default for InstallationData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
installation_id: Uuid::new_v4().to_string(),
|
||||
first_seen: Utc::now(),
|
||||
session_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn installation_file_path() -> std::path::PathBuf {
|
||||
Paths::state_dir().join("telemetry_installation.json")
|
||||
}
|
||||
|
||||
fn load_or_create_installation() -> InstallationData {
|
||||
let path = installation_file_path();
|
||||
|
||||
if let Ok(contents) = fs::read_to_string(&path) {
|
||||
if let Ok(data) = serde_json::from_str::<InstallationData>(&contents) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
let data = InstallationData::default();
|
||||
save_installation(&data);
|
||||
data
|
||||
}
|
||||
|
||||
fn save_installation(data: &InstallationData) {
|
||||
let path = installation_file_path();
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
if let Ok(json) = serde_json::to_string_pretty(data) {
|
||||
let _ = fs::write(path, json);
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_session_count() -> InstallationData {
|
||||
let mut data = load_or_create_installation();
|
||||
data.session_count += 1;
|
||||
save_installation(&data);
|
||||
data
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Platform Info
|
||||
// ============================================================================
|
||||
|
||||
fn get_platform_version() -> Option<String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
std::process::Command::new("sw_vers")
|
||||
.arg("-productVersion")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
fs::read_to_string("/etc/os-release")
|
||||
.ok()
|
||||
.and_then(|content| {
|
||||
content
|
||||
.lines()
|
||||
.find(|line| line.starts_with("VERSION_ID="))
|
||||
.map(|line| {
|
||||
line.trim_start_matches("VERSION_ID=")
|
||||
.trim_matches('"')
|
||||
.to_string()
|
||||
})
|
||||
})
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
std::process::Command::new("cmd")
|
||||
.args(["/C", "ver"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_install_method() -> String {
|
||||
let exe_path = std::env::current_exe().ok();
|
||||
|
||||
if let Some(path) = exe_path {
|
||||
let path_str = path.to_string_lossy().to_lowercase();
|
||||
|
||||
if path_str.contains("homebrew") || path_str.contains("/opt/homebrew") {
|
||||
return "homebrew".to_string();
|
||||
}
|
||||
if path_str.contains(".cargo") {
|
||||
return "cargo".to_string();
|
||||
}
|
||||
if path_str.contains("applications") || path_str.contains(".app") {
|
||||
return "desktop".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if std::env::var("GOOSE_DESKTOP").is_ok() {
|
||||
return "desktop".to_string();
|
||||
}
|
||||
|
||||
"binary".to_string()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session Context (set by CLI/Desktop at startup)
|
||||
// ============================================================================
|
||||
|
||||
static SESSION_INTERFACE: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||
static SESSION_IS_RESUMED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn set_session_context(interface: &str, is_resumed: bool) {
|
||||
if let Ok(mut iface) = SESSION_INTERFACE.lock() {
|
||||
*iface = Some(interface.to_string());
|
||||
}
|
||||
SESSION_IS_RESUMED.store(is_resumed, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn get_session_interface() -> String {
|
||||
SESSION_INTERFACE
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|i| i.clone())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
fn get_session_is_resumed() -> bool {
|
||||
SESSION_IS_RESUMED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Telemetry Events
|
||||
// ============================================================================
|
||||
|
||||
pub fn emit_session_started() {
|
||||
if !is_telemetry_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let installation = increment_session_count();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = send_session_event(&installation).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn emit_error(error_type: &str) {
|
||||
if !is_telemetry_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let installation = load_or_create_installation();
|
||||
let error_type = error_type.to_string();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = send_error_event(&installation, &error_type).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn send_error_event(installation: &InstallationData, error_type: &str) -> Result<(), String> {
|
||||
let client = posthog_rs::client(POSTHOG_API_KEY).await;
|
||||
let mut event = posthog_rs::Event::new("error", &installation.installation_id);
|
||||
|
||||
event.insert_prop("error_type", error_type).ok();
|
||||
event.insert_prop("version", env!("CARGO_PKG_VERSION")).ok();
|
||||
event.insert_prop("interface", get_session_interface()).ok();
|
||||
event.insert_prop("os", std::env::consts::OS).ok();
|
||||
event.insert_prop("arch", std::env::consts::ARCH).ok();
|
||||
|
||||
if let Some(platform_version) = get_platform_version() {
|
||||
event.insert_prop("platform_version", platform_version).ok();
|
||||
}
|
||||
|
||||
let config = Config::global();
|
||||
if let Ok(provider) = config.get_param::<String>("GOOSE_PROVIDER") {
|
||||
event.insert_prop("provider", provider).ok();
|
||||
}
|
||||
if let Ok(model) = config.get_param::<String>("GOOSE_MODEL") {
|
||||
event.insert_prop("model", model).ok();
|
||||
}
|
||||
|
||||
client.capture(event).await.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
async fn send_session_event(installation: &InstallationData) -> Result<(), String> {
|
||||
let client = posthog_rs::client(POSTHOG_API_KEY).await;
|
||||
let mut event = posthog_rs::Event::new("session_started", &installation.installation_id);
|
||||
|
||||
event.insert_prop("os", std::env::consts::OS).ok();
|
||||
event.insert_prop("arch", std::env::consts::ARCH).ok();
|
||||
event.insert_prop("version", env!("CARGO_PKG_VERSION")).ok();
|
||||
|
||||
if let Some(platform_version) = get_platform_version() {
|
||||
event.insert_prop("platform_version", platform_version).ok();
|
||||
}
|
||||
|
||||
event
|
||||
.insert_prop("install_method", detect_install_method())
|
||||
.ok();
|
||||
|
||||
event.insert_prop("interface", get_session_interface()).ok();
|
||||
|
||||
event
|
||||
.insert_prop("is_resumed", get_session_is_resumed())
|
||||
.ok();
|
||||
|
||||
event
|
||||
.insert_prop("session_number", installation.session_count)
|
||||
.ok();
|
||||
let days_since_install = (Utc::now() - installation.first_seen).num_days();
|
||||
event
|
||||
.insert_prop("days_since_install", days_since_install)
|
||||
.ok();
|
||||
|
||||
let config = Config::global();
|
||||
if let Ok(provider) = config.get_param::<String>("GOOSE_PROVIDER") {
|
||||
event.insert_prop("provider", provider).ok();
|
||||
}
|
||||
if let Ok(model) = config.get_param::<String>("GOOSE_MODEL") {
|
||||
event.insert_prop("model", model).ok();
|
||||
}
|
||||
|
||||
let extensions = get_enabled_extensions();
|
||||
event.insert_prop("extensions_count", extensions.len()).ok();
|
||||
let extension_names: Vec<String> = extensions.iter().map(|e| e.name()).collect();
|
||||
event.insert_prop("extensions", extension_names).ok();
|
||||
|
||||
if let Ok(insights) = SessionManager::get_insights().await {
|
||||
event
|
||||
.insert_prop("total_sessions", insights.total_sessions)
|
||||
.ok();
|
||||
event
|
||||
.insert_prop("total_tokens", insights.total_tokens)
|
||||
.ok();
|
||||
}
|
||||
|
||||
client.capture(event).await.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
use futures::Stream;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::canonical::{map_to_canonical_model, CanonicalModelRegistry};
|
||||
use super::errors::ProviderError;
|
||||
use super::retry::RetryConfig;
|
||||
use crate::config::base::ConfigValue;
|
||||
@@ -415,6 +416,52 @@ pub trait Provider: Send + Sync {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Fetch models filtered by canonical registry and usability
|
||||
async fn fetch_recommended_models(&self) -> Result<Option<Vec<String>>, ProviderError> {
|
||||
let all_models = match self.fetch_supported_models().await? {
|
||||
Some(models) => models,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let registry = CanonicalModelRegistry::bundled().map_err(|e| {
|
||||
ProviderError::ExecutionError(format!("Failed to load canonical registry: {}", e))
|
||||
})?;
|
||||
|
||||
let provider_name = self.get_name();
|
||||
|
||||
let recommended_models: Vec<String> = all_models
|
||||
.iter()
|
||||
.filter(|model| {
|
||||
map_to_canonical_model(provider_name, model, registry)
|
||||
.and_then(|canonical_id| registry.get(&canonical_id))
|
||||
.map(|m| m.input_modalities.contains(&"text".to_string()))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if recommended_models.is_empty() {
|
||||
Ok(Some(all_models))
|
||||
} else {
|
||||
Ok(Some(recommended_models))
|
||||
}
|
||||
}
|
||||
|
||||
async fn map_to_canonical_model(
|
||||
&self,
|
||||
provider_model: &str,
|
||||
) -> Result<Option<String>, ProviderError> {
|
||||
let registry = CanonicalModelRegistry::bundled().map_err(|e| {
|
||||
ProviderError::ExecutionError(format!("Failed to load canonical registry: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(map_to_canonical_model(
|
||||
self.get_name(),
|
||||
provider_model,
|
||||
registry,
|
||||
))
|
||||
}
|
||||
|
||||
fn supports_embeddings(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Canonical Model System
|
||||
|
||||
Provides a unified view of model metadata (pricing, capabilities, context limits) across different LLM providers.
|
||||
Normalizes provider-specific model names (e.g., `claude-3-5-sonnet-20241022`)
|
||||
to canonical IDs (e.g., `anthropic/claude-3.5-sonnet`).
|
||||
|
||||
## Scripts
|
||||
|
||||
### Build Canonical Models
|
||||
Fetches latest model metadata from OpenRouter and updates the registry:
|
||||
```bash
|
||||
cargo run --example build_canonical_models
|
||||
```
|
||||
Writes to: `src/providers/canonical/data/canonical_models.json`
|
||||
|
||||
### Check Model Mappings
|
||||
Tests provider model mappings and tracks changes over time:
|
||||
```bash
|
||||
cargo run --example canonical_model_checker
|
||||
```
|
||||
- Reports unmapped models
|
||||
- Compares with previous runs (like a lock file)
|
||||
- Shows changed/added/removed mappings
|
||||
- Writes to: `src/providers/canonical/data/canonical_mapping_report.json`
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
mod model;
|
||||
mod name_builder;
|
||||
mod registry;
|
||||
|
||||
pub use model::{CanonicalModel, Pricing};
|
||||
pub use name_builder::{canonical_name, map_to_canonical_model, strip_version_suffix};
|
||||
pub use registry::CanonicalModelRegistry;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ModelMapping {
|
||||
pub provider_model: String,
|
||||
pub canonical_model: String,
|
||||
}
|
||||
|
||||
impl ModelMapping {
|
||||
pub fn new(provider_model: impl Into<String>, canonical_model: impl Into<String>) -> Self {
|
||||
Self {
|
||||
provider_model: provider_model.into(),
|
||||
canonical_model: canonical_model.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Pricing information for a model (all costs in USD per token)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Pricing {
|
||||
/// Cost per prompt token
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompt: Option<f64>,
|
||||
|
||||
/// Cost per completion token
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completion: Option<f64>,
|
||||
|
||||
/// Cost per request
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub request: Option<f64>,
|
||||
|
||||
/// Cost per image
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<f64>,
|
||||
}
|
||||
|
||||
/// Canonical representation of a model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CanonicalModel {
|
||||
/// Model identifier (e.g., "anthropic/claude-3-5-sonnet" or "openai/gpt-4o:extended")
|
||||
pub id: String,
|
||||
|
||||
/// Human-readable name (e.g., "Claude 3.5 Sonnet")
|
||||
pub name: String,
|
||||
|
||||
/// Maximum context window size in tokens
|
||||
pub context_length: usize,
|
||||
|
||||
/// Maximum completion tokens
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_completion_tokens: Option<usize>,
|
||||
|
||||
/// Input modalities supported (e.g., ["text", "image"])
|
||||
#[serde(default)]
|
||||
pub input_modalities: Vec<String>,
|
||||
|
||||
/// Output modalities supported (e.g., ["text"])
|
||||
#[serde(default)]
|
||||
pub output_modalities: Vec<String>,
|
||||
|
||||
/// Whether the model supports tool calling
|
||||
#[serde(default)]
|
||||
pub supports_tools: bool,
|
||||
|
||||
/// Pricing for this model
|
||||
pub pricing: Pricing,
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
static NORMALIZE_VERSION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"-(\d)-(\d)(-|$)").unwrap());
|
||||
|
||||
static STRIP_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
|
||||
vec![
|
||||
Regex::new(r"-latest$").unwrap(),
|
||||
Regex::new(r"-preview(-\d+)*$").unwrap(),
|
||||
Regex::new(r"-exp(-\d+)*$").unwrap(),
|
||||
Regex::new(r":exacto$").unwrap(),
|
||||
Regex::new(r"-\d{8}$").unwrap(),
|
||||
Regex::new(r"-\d{4}-\d{2}-\d{2}$").unwrap(),
|
||||
Regex::new(r"-v\d+(\.\d+)*$").unwrap(),
|
||||
Regex::new(r"-\d{3,}$").unwrap(),
|
||||
Regex::new(r"-bedrock$").unwrap(),
|
||||
]
|
||||
});
|
||||
|
||||
static CLAUDE_PATTERNS: Lazy<Vec<(Regex, Regex, &'static str)>> = Lazy::new(|| {
|
||||
["sonnet", "opus", "haiku"]
|
||||
.iter()
|
||||
.map(|&size| {
|
||||
(
|
||||
Regex::new(&format!("claude-([0-9.-]+)-{}", size)).unwrap(),
|
||||
Regex::new(&format!("claude-{}-([0-9.-]+)", size)).unwrap(),
|
||||
size,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
/// Build canonical model name from provider and model identifiers
|
||||
pub fn canonical_name(provider: &str, model: &str) -> String {
|
||||
let model_base = strip_version_suffix(model);
|
||||
|
||||
// OpenRouter models are already in canonical format
|
||||
if provider == "openrouter" {
|
||||
model_base
|
||||
} else {
|
||||
format!("{}/{}", provider, model_base)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to build a canonical name and check if it exists in the registry
|
||||
fn try_canonical(
|
||||
provider: &str,
|
||||
model: &str,
|
||||
registry: &super::CanonicalModelRegistry,
|
||||
) -> Option<String> {
|
||||
let candidate = canonical_name(provider, model);
|
||||
registry.get(&candidate).map(|_| candidate)
|
||||
}
|
||||
|
||||
/// Try to map a provider/model pair to a canonical model
|
||||
pub fn map_to_canonical_model(
|
||||
provider: &str,
|
||||
model: &str,
|
||||
registry: &super::CanonicalModelRegistry,
|
||||
) -> Option<String> {
|
||||
// Try direct mapping first
|
||||
if let Some(candidate) = try_canonical(provider, model, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
// Try with common prefixes stripped
|
||||
let model_stripped = strip_common_prefixes(model);
|
||||
if model_stripped != model {
|
||||
if let Some(candidate) = try_canonical(provider, &model_stripped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// Try word-order swapping for Claude models (claude-4-opus ↔ claude-opus-4)
|
||||
if let Some(swapped) = swap_claude_word_order(&model_stripped) {
|
||||
if let Some(candidate) = try_canonical(provider, &swapped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
if is_hosting_provider(provider) {
|
||||
if let Some(inferred) = infer_provider_from_model(&swapped) {
|
||||
if let Some(candidate) = try_canonical(inferred, &swapped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For hosting providers, try to infer the real provider from model name patterns
|
||||
if is_hosting_provider(provider) {
|
||||
if let Some(inferred_provider) = infer_provider_from_model(&model_stripped) {
|
||||
if let Some(candidate) = try_canonical(inferred_provider, &model_stripped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(inferred) = infer_provider_from_model(model) {
|
||||
if let Some(candidate) = try_canonical(inferred, model, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For provider-prefixed models like "databricks-meta-llama-3-1-70b"
|
||||
if let Some((extracted_provider, extracted_model)) = extract_provider_prefix(&model_stripped) {
|
||||
if let Some(candidate) = try_canonical(extracted_provider, extracted_model, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Swap word order for Claude models to handle both naming conventions
|
||||
fn swap_claude_word_order(model: &str) -> Option<String> {
|
||||
if !model.starts_with("claude-") {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (forward_re, reverse_re, size) in CLAUDE_PATTERNS.iter() {
|
||||
if let Some(captures) = forward_re.captures(model) {
|
||||
let version = &captures[1];
|
||||
return Some(format!("claude-{}-{}", size, version));
|
||||
}
|
||||
|
||||
if let Some(captures) = reverse_re.captures(model) {
|
||||
let version = &captures[1];
|
||||
return Some(format!("claude-{}-{}", version, size));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_hosting_provider(provider: &str) -> bool {
|
||||
matches!(provider, "databricks" | "openrouter" | "azure" | "bedrock")
|
||||
}
|
||||
|
||||
/// Infer the real provider from model name patterns
|
||||
fn infer_provider_from_model(model: &str) -> Option<&'static str> {
|
||||
let model_lower = model.to_lowercase();
|
||||
|
||||
if model_lower.contains("claude") {
|
||||
return Some("anthropic");
|
||||
}
|
||||
|
||||
if model_lower.starts_with("gpt-")
|
||||
|| model_lower.starts_with("o1")
|
||||
|| model_lower.starts_with("o3")
|
||||
|| model_lower.starts_with("o4")
|
||||
|| model_lower.starts_with("chatgpt-")
|
||||
{
|
||||
return Some("openai");
|
||||
}
|
||||
|
||||
if model_lower.starts_with("gemini-") || model_lower.starts_with("gemma-") {
|
||||
return Some("google");
|
||||
}
|
||||
|
||||
if model_lower.contains("llama") {
|
||||
return Some("meta-llama");
|
||||
}
|
||||
|
||||
if model_lower.starts_with("mistral")
|
||||
|| model_lower.starts_with("mixtral")
|
||||
|| model_lower.starts_with("codestral")
|
||||
|| model_lower.starts_with("ministral")
|
||||
|| model_lower.starts_with("pixtral")
|
||||
|| model_lower.starts_with("devstral")
|
||||
|| model_lower.starts_with("voxtral")
|
||||
{
|
||||
return Some("mistralai");
|
||||
}
|
||||
|
||||
if model_lower.contains("deepseek") {
|
||||
return Some("deepseek");
|
||||
}
|
||||
|
||||
if model_lower.contains("qwen") {
|
||||
return Some("qwen");
|
||||
}
|
||||
|
||||
if model_lower.contains("grok") {
|
||||
return Some("x-ai");
|
||||
}
|
||||
|
||||
if model_lower.contains("jamba") {
|
||||
return Some("ai21");
|
||||
}
|
||||
|
||||
if model_lower.contains("command") {
|
||||
return Some("cohere");
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Strip common prefixes from model names using pattern matching
|
||||
/// Looks for known model family patterns and strips everything before them
|
||||
fn strip_common_prefixes(model: &str) -> String {
|
||||
let model_patterns = [
|
||||
"claude-",
|
||||
"gpt-",
|
||||
"gemini-",
|
||||
"gemma-",
|
||||
"o1-",
|
||||
"o1",
|
||||
"o3-",
|
||||
"o3",
|
||||
"o4-",
|
||||
"llama-",
|
||||
"mistral-",
|
||||
"mixtral-",
|
||||
"chatgpt-",
|
||||
"deepseek-",
|
||||
"qwen-",
|
||||
"grok-",
|
||||
"jamba-",
|
||||
"command-",
|
||||
"codestral",
|
||||
"ministral-",
|
||||
"pixtral-",
|
||||
"devstral-",
|
||||
];
|
||||
|
||||
let mut earliest_pos = None;
|
||||
|
||||
for pattern in &model_patterns {
|
||||
if let Some(pos) = model.to_lowercase().find(pattern) {
|
||||
if earliest_pos.is_none() || pos < earliest_pos.unwrap() {
|
||||
earliest_pos = Some(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a pattern, strip everything before it
|
||||
if let Some(pos) = earliest_pos {
|
||||
return model.get(pos..).unwrap_or(model).to_string();
|
||||
}
|
||||
|
||||
model.to_string()
|
||||
}
|
||||
|
||||
/// Try to extract provider prefix from model names like "databricks-meta-llama-3-1-70b"
|
||||
/// Returns (provider, model) tuple if found
|
||||
fn extract_provider_prefix(model: &str) -> Option<(&'static str, &str)> {
|
||||
let known_providers = [
|
||||
"anthropic",
|
||||
"openai",
|
||||
"google",
|
||||
"meta-llama",
|
||||
"mistralai",
|
||||
"cohere",
|
||||
"ai21",
|
||||
"amazon",
|
||||
"deepseek",
|
||||
"qwen",
|
||||
"x-ai",
|
||||
"nvidia",
|
||||
"microsoft",
|
||||
"perplexity",
|
||||
];
|
||||
|
||||
for provider in &known_providers {
|
||||
let prefix = format!("{}-", provider);
|
||||
if model.starts_with(&prefix) {
|
||||
if let Some(model_part) = model.strip_prefix(&prefix) {
|
||||
return Some((provider, model_part));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Strip version suffixes from model names and normalize version numbers
|
||||
pub fn strip_version_suffix(model: &str) -> String {
|
||||
let mut result = NORMALIZE_VERSION_RE
|
||||
.replace_all(model, "-$1.$2$3")
|
||||
.to_string();
|
||||
|
||||
let mut changed = true;
|
||||
while changed {
|
||||
let before = result.clone();
|
||||
for pattern in STRIP_PATTERNS.iter() {
|
||||
result = pattern.replace(&result, "").to_string();
|
||||
}
|
||||
changed = result != before;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_map_to_canonical_model() {
|
||||
let r = super::super::CanonicalModelRegistry::bundled().unwrap();
|
||||
|
||||
// === Direct provider (non-hosting) ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("anthropic", "claude-3-5-sonnet-20241022", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("openai", "gpt-4o-latest", r),
|
||||
Some("openai/gpt-4o".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("openai", "gpt-4-turbo-2024-04-09", r),
|
||||
Some("openai/gpt-4-turbo".to_string())
|
||||
);
|
||||
|
||||
// === OpenRouter (already canonical format) ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("openrouter", "anthropic/claude-3.5-sonnet", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
);
|
||||
|
||||
// === Anthropic Claude - basic ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-3-5-sonnet", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-3-5-sonnet-20241022", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-3-5-sonnet-latest", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
);
|
||||
|
||||
// === Claude word-order swapping (3.x series) ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-haiku-3-5", r),
|
||||
Some("anthropic/claude-3.5-haiku".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-sonnet-3-7", r),
|
||||
Some("anthropic/claude-3.7-sonnet".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "ng-tools-claude-haiku-3-5", r),
|
||||
Some("anthropic/claude-3.5-haiku".to_string())
|
||||
);
|
||||
|
||||
// === Claude word-order swapping (4.x series) ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-4-opus", r),
|
||||
Some("anthropic/claude-opus-4".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-4-sonnet", r),
|
||||
Some("anthropic/claude-sonnet-4".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "raml-claude-opus-4-5", r),
|
||||
Some("anthropic/claude-opus-4.5".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "databricks-claude-sonnet-4-5", r),
|
||||
Some("anthropic/claude-sonnet-4.5".to_string())
|
||||
);
|
||||
|
||||
// === Claude with custom prefixes ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "goose-claude-4-opus", r),
|
||||
Some("anthropic/claude-opus-4".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "kgoose-claude-4-sonnet", r),
|
||||
Some("anthropic/claude-sonnet-4".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "headless-goose-claude-4-sonnet", r),
|
||||
Some("anthropic/claude-sonnet-4".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "kgoose-cashapp-claude-4-sonnet", r),
|
||||
Some("anthropic/claude-sonnet-4".to_string())
|
||||
);
|
||||
|
||||
// === Claude with platform suffixes ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "claude-4-sonnet-bedrock", r),
|
||||
Some("anthropic/claude-sonnet-4".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "goose-claude-4-sonnet-bedrock", r),
|
||||
Some("anthropic/claude-sonnet-4".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("bedrock", "claude-3-5-sonnet", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
);
|
||||
|
||||
// === OpenAI GPT ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "gpt-4o", r),
|
||||
Some("openai/gpt-4o".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "gpt-4o-2024-11-20", r),
|
||||
Some("openai/gpt-4o".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "gpt-4o-latest", r),
|
||||
Some("openai/gpt-4o".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "kgoose-gpt-4o", r),
|
||||
Some("openai/gpt-4o".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("azure", "gpt-4o", r),
|
||||
Some("openai/gpt-4o".to_string())
|
||||
);
|
||||
|
||||
// === OpenAI O-series ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "goose-o1", r),
|
||||
Some("openai/o1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "kgoose-o3", r),
|
||||
Some("openai/o3".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "headless-goose-o3-mini", r),
|
||||
Some("openai/o3-mini".to_string())
|
||||
);
|
||||
|
||||
// === Google Gemini ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "gemini-2-5-flash", r),
|
||||
Some("google/gemini-2.5-flash".to_string())
|
||||
);
|
||||
|
||||
// === Meta Llama ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "meta-llama-3-1-70b-instruct", r),
|
||||
Some("meta-llama/llama-3.1-70b-instruct".to_string())
|
||||
);
|
||||
|
||||
// === Mistral variants ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "codestral", r),
|
||||
Some("mistralai/codestral".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "ministral-8b", r),
|
||||
Some("mistralai/ministral-8b".to_string())
|
||||
);
|
||||
|
||||
// === DeepSeek ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "databricks-deepseek-chat", r),
|
||||
Some("deepseek/deepseek-chat".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "deepseek-r1", r),
|
||||
Some("deepseek/deepseek-r1".to_string())
|
||||
);
|
||||
|
||||
// === Qwen ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "qwen-2-5-72b-instruct", r),
|
||||
Some("qwen/qwen-2.5-72b-instruct".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "goose-qwen-2-5-72b-instruct", r),
|
||||
Some("qwen/qwen-2.5-72b-instruct".to_string())
|
||||
);
|
||||
|
||||
// === Grok (X.AI) ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "grok-3", r),
|
||||
Some("x-ai/grok-3".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "databricks-grok-4-fast", r),
|
||||
Some("x-ai/grok-4-fast".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "kgoose-grok-4-fast", r),
|
||||
Some("x-ai/grok-4-fast".to_string())
|
||||
);
|
||||
|
||||
// === Jamba (AI21) ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "jamba-large-1-7", r),
|
||||
Some("ai21/jamba-large-1.7".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "databricks-jamba-large-1-7", r),
|
||||
Some("ai21/jamba-large-1.7".to_string())
|
||||
);
|
||||
|
||||
// === Cohere Command ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "command-r-plus-08", r),
|
||||
Some("cohere/command-r-plus-08".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "goose-command-r-08", r),
|
||||
Some("cohere/command-r-08".to_string())
|
||||
);
|
||||
|
||||
// === Provider-prefixed extraction ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "anthropic-claude-3-5-sonnet", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "openai-gpt-4o", r),
|
||||
Some("openai/gpt-4o".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "google-gemini-2-5-flash", r),
|
||||
Some("google/gemini-2.5-flash".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "mistralai-mistral-large", r),
|
||||
Some("mistralai/mistral-large".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "deepseek-deepseek-chat", r),
|
||||
Some("deepseek/deepseek-chat".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "qwen-qwen-2-5-72b-instruct", r),
|
||||
Some("qwen/qwen-2.5-72b-instruct".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "x-ai-grok-3", r),
|
||||
Some("x-ai/grok-3".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use super::CanonicalModel;
|
||||
use anyhow::{Context, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// Cached bundled canonical model registry
|
||||
static BUNDLED_REGISTRY: Lazy<Result<CanonicalModelRegistry>> = Lazy::new(|| {
|
||||
const CANONICAL_MODELS_JSON: &str = include_str!("data/canonical_models.json");
|
||||
|
||||
let models: Vec<CanonicalModel> = serde_json::from_str(CANONICAL_MODELS_JSON)
|
||||
.context("Failed to parse bundled canonical models JSON")?;
|
||||
|
||||
let mut registry = CanonicalModelRegistry::new();
|
||||
for model in models {
|
||||
registry.register(model);
|
||||
}
|
||||
|
||||
Ok(registry)
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CanonicalModelRegistry {
|
||||
models: HashMap<String, CanonicalModel>,
|
||||
}
|
||||
|
||||
impl CanonicalModelRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
models: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bundled() -> Result<&'static Self> {
|
||||
BUNDLED_REGISTRY
|
||||
.as_ref()
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))
|
||||
}
|
||||
|
||||
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path.as_ref())
|
||||
.context("Failed to read canonical models file")?;
|
||||
|
||||
let models: Vec<CanonicalModel> =
|
||||
serde_json::from_str(&content).context("Failed to parse canonical models JSON")?;
|
||||
|
||||
let mut registry = Self::new();
|
||||
for model in models {
|
||||
registry.register(model);
|
||||
}
|
||||
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
pub fn to_file(&self, path: impl AsRef<Path>) -> Result<()> {
|
||||
let mut models: Vec<&CanonicalModel> = self.models.values().collect();
|
||||
models.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
|
||||
let json = serde_json::to_string_pretty(&models)
|
||||
.context("Failed to serialize canonical models")?;
|
||||
|
||||
std::fs::write(path.as_ref(), json).context("Failed to write canonical models file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register(&mut self, model: CanonicalModel) {
|
||||
self.models.insert(model.id.clone(), model);
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&CanonicalModel> {
|
||||
self.models.get(name)
|
||||
}
|
||||
|
||||
pub fn all_models(&self) -> Vec<&CanonicalModel> {
|
||||
self.models.values().collect()
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.models.len()
|
||||
}
|
||||
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.models.contains_key(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CanonicalModelRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,10 @@ impl ClaudeCodeProvider {
|
||||
}
|
||||
}
|
||||
MessageContent::ToolResponse(tool_response) => {
|
||||
if let Ok(tool_contents) = &tool_response.tool_result {
|
||||
if let Ok(result) = &tool_response.tool_result {
|
||||
// Convert tool result contents to text
|
||||
let content_text = tool_contents
|
||||
let content_text = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| match &content.raw {
|
||||
rmcp::model::RawContent::Text(text_content) => {
|
||||
|
||||
@@ -86,8 +86,9 @@ impl CursorAgentProvider {
|
||||
}
|
||||
}
|
||||
MessageContent::ToolResponse(tool_response) => {
|
||||
if let Ok(tool_contents) = &tool_response.tool_result {
|
||||
let content_text = tool_contents
|
||||
if let Ok(result) = &tool_response.tool_result {
|
||||
let content_text = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| match &content.raw {
|
||||
rmcp::model::RawContent::Text(text_content) => {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "inception",
|
||||
"engine": "openai",
|
||||
"display_name": "Inception",
|
||||
"description": "Mercury models from Inception leveraging diffusion for lightning speeds",
|
||||
"api_key_env": "INCEPTION_API_KEY",
|
||||
"base_url": "https://api.inceptionlabs.ai",
|
||||
"models": [
|
||||
{
|
||||
"name": "mercury-coder",
|
||||
"context_limit": 128000
|
||||
}
|
||||
],
|
||||
"supports_streaming": true
|
||||
}
|
||||
@@ -32,6 +32,21 @@ pub enum ProviderError {
|
||||
NotImplemented(String),
|
||||
}
|
||||
|
||||
impl ProviderError {
|
||||
pub fn telemetry_type(&self) -> &'static str {
|
||||
match self {
|
||||
ProviderError::Authentication(_) => "auth",
|
||||
ProviderError::ContextLengthExceeded(_) => "context_length",
|
||||
ProviderError::RateLimitExceeded { .. } => "rate_limit",
|
||||
ProviderError::ServerError(_) => "server",
|
||||
ProviderError::RequestFailed(_) => "request",
|
||||
ProviderError::ExecutionError(_) => "execution",
|
||||
ProviderError::UsageError(_) => "usage",
|
||||
ProviderError::NotImplemented(_) => "not_implemented",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ProviderError {
|
||||
fn from(error: anyhow::Error) -> Self {
|
||||
if let Some(reqwest_err) = error.downcast_ref::<reqwest::Error>() {
|
||||
|
||||
@@ -68,6 +68,7 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
MessageContent::ToolResponse(tool_response) => match &tool_response.tool_result {
|
||||
Ok(result) => {
|
||||
let text = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|c| c.as_text().map(|t| t.text.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -86,8 +86,9 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
|
||||
}
|
||||
MessageContent::ToolResponse(tool_res) => {
|
||||
let content = match &tool_res.tool_result {
|
||||
Ok(content) => Some(
|
||||
content
|
||||
Ok(result) => Some(
|
||||
result
|
||||
.content
|
||||
.iter()
|
||||
// Filter out content items that have User in their audience
|
||||
.filter(|c| {
|
||||
@@ -318,6 +319,12 @@ pub fn from_bedrock_content_block(block: &bedrock::ContentBlock) -> Result<Messa
|
||||
.iter()
|
||||
.map(from_bedrock_tool_result_content_block)
|
||||
.collect::<ToolResult<Vec<_>>>()
|
||||
.map(|content| rmcp::model::CallToolResult {
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
},
|
||||
),
|
||||
_ => bail!("Unsupported content block type from Bedrock"),
|
||||
|
||||
@@ -133,9 +133,10 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<Data
|
||||
}
|
||||
MessageContent::ToolResponse(response) => {
|
||||
match &response.tool_result {
|
||||
Ok(contents) => {
|
||||
Ok(call_result) => {
|
||||
// Send only contents with no audience or with Assistant in the audience
|
||||
let abridged: Vec<_> = contents
|
||||
let abridged: Vec<_> = call_result
|
||||
.content
|
||||
.iter()
|
||||
.filter(|content| {
|
||||
content
|
||||
@@ -533,12 +534,7 @@ pub fn create_request(
|
||||
};
|
||||
|
||||
let system_message = DatabricksMessage {
|
||||
role: if is_openai_reasoning_model {
|
||||
"developer"
|
||||
} else {
|
||||
"system"
|
||||
}
|
||||
.to_string(),
|
||||
role: "system".to_string(),
|
||||
content: system.into(),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
@@ -638,6 +634,7 @@ pub fn create_request(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conversation::message::Message;
|
||||
use rmcp::model::CallToolResult;
|
||||
use rmcp::object;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -727,8 +724,15 @@ mod tests {
|
||||
panic!("should be tool request");
|
||||
};
|
||||
|
||||
messages
|
||||
.push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")])));
|
||||
messages.push(Message::user().with_tool_response(
|
||||
tool_id,
|
||||
Ok(CallToolResult {
|
||||
content: vec![Content::text("Result")],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
));
|
||||
|
||||
let as_value =
|
||||
serde_json::to_value(format_messages(&messages, &ImageFormat::OpenAi)).unwrap();
|
||||
@@ -764,8 +768,15 @@ mod tests {
|
||||
panic!("should be tool request");
|
||||
};
|
||||
|
||||
messages
|
||||
.push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")])));
|
||||
messages.push(Message::user().with_tool_response(
|
||||
tool_id,
|
||||
Ok(CallToolResult {
|
||||
content: vec![Content::text("Result")],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
));
|
||||
|
||||
let as_value =
|
||||
serde_json::to_value(format_messages(&messages, &ImageFormat::OpenAi)).unwrap();
|
||||
@@ -1007,41 +1018,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_request_o1_default() -> anyhow::Result<()> {
|
||||
// Test default medium reasoning effort for O1 model
|
||||
let model_config = ModelConfig {
|
||||
model_name: "o1".to_string(),
|
||||
context_limit: Some(4096),
|
||||
temperature: None,
|
||||
max_tokens: Some(1024),
|
||||
toolshim: false,
|
||||
toolshim_model: None,
|
||||
fast_model: None,
|
||||
};
|
||||
let request = create_request(&model_config, "system", &[], &[], &ImageFormat::OpenAi)?;
|
||||
let obj = request.as_object().unwrap();
|
||||
let expected = json!({
|
||||
"model": "o1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "developer",
|
||||
"content": "system"
|
||||
}
|
||||
],
|
||||
"reasoning_effort": "medium",
|
||||
"max_completion_tokens": 1024
|
||||
});
|
||||
|
||||
for (key, value) in expected.as_object().unwrap() {
|
||||
assert_eq!(obj.get(key).unwrap(), value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_request_o3_custom_reasoning_effort() -> anyhow::Result<()> {
|
||||
// Test custom reasoning effort for O3 model
|
||||
fn test_create_request_reasoning_effort() -> anyhow::Result<()> {
|
||||
let model_config = ModelConfig {
|
||||
model_name: "o3-mini-high".to_string(),
|
||||
context_limit: Some(4096),
|
||||
@@ -1052,23 +1029,7 @@ mod tests {
|
||||
fast_model: None,
|
||||
};
|
||||
let request = create_request(&model_config, "system", &[], &[], &ImageFormat::OpenAi)?;
|
||||
let obj = request.as_object().unwrap();
|
||||
let expected = json!({
|
||||
"model": "o3-mini",
|
||||
"messages": [
|
||||
{
|
||||
"role": "developer",
|
||||
"content": "system"
|
||||
}
|
||||
],
|
||||
"reasoning_effort": "high",
|
||||
"max_completion_tokens": 1024
|
||||
});
|
||||
|
||||
for (key, value) in expected.as_object().unwrap() {
|
||||
assert_eq!(obj.get(key).unwrap(), value);
|
||||
}
|
||||
|
||||
assert_eq!(request["reasoning_effort"], "high");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -70,9 +70,10 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
},
|
||||
MessageContent::ToolResponse(response) => {
|
||||
match &response.tool_result {
|
||||
Ok(contents) => {
|
||||
Ok(result) => {
|
||||
// Send only contents with no audience or with Assistant in the audience
|
||||
let abridged: Vec<_> = contents
|
||||
let abridged: Vec<_> = result
|
||||
.content
|
||||
.iter()
|
||||
.filter(|content| {
|
||||
content.audience().is_none_or(|audience| {
|
||||
@@ -394,7 +395,7 @@ pub fn create_request(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conversation::message::Message;
|
||||
use rmcp::model::CallToolRequestParam;
|
||||
use rmcp::model::{CallToolRequestParam, CallToolResult};
|
||||
use rmcp::{model::Content, object};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -429,7 +430,12 @@ mod tests {
|
||||
0,
|
||||
vec![MessageContent::tool_response(
|
||||
id.to_string(),
|
||||
Ok(tool_response),
|
||||
Ok(CallToolResult {
|
||||
content: tool_response,
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod databricks;
|
||||
pub mod gcpvertexai;
|
||||
pub mod google;
|
||||
pub mod openai;
|
||||
pub mod openai_responses;
|
||||
pub mod snowflake;
|
||||
|
||||
@@ -128,9 +128,10 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
||||
},
|
||||
MessageContent::ToolResponse(response) => {
|
||||
match &response.tool_result {
|
||||
Ok(contents) => {
|
||||
Ok(result) => {
|
||||
// Send only contents with no audience or with Assistant in the audience
|
||||
let abridged: Vec<_> = contents
|
||||
let abridged: Vec<_> = result
|
||||
.content
|
||||
.iter()
|
||||
.filter(|content| {
|
||||
content
|
||||
@@ -710,6 +711,7 @@ pub fn create_request(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conversation::message::Message;
|
||||
use rmcp::model::CallToolResult;
|
||||
use rmcp::object;
|
||||
use serde_json::json;
|
||||
use tokio::pin;
|
||||
@@ -868,8 +870,15 @@ mod tests {
|
||||
panic!("should be tool request");
|
||||
};
|
||||
|
||||
messages
|
||||
.push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")])));
|
||||
messages.push(Message::user().with_tool_response(
|
||||
tool_id,
|
||||
Ok(CallToolResult {
|
||||
content: vec![Content::text("Result")],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
));
|
||||
|
||||
let spec = format_messages(&messages, &ImageFormat::OpenAi);
|
||||
|
||||
@@ -904,8 +913,15 @@ mod tests {
|
||||
panic!("should be tool request");
|
||||
};
|
||||
|
||||
messages
|
||||
.push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")])));
|
||||
messages.push(Message::user().with_tool_response(
|
||||
tool_id,
|
||||
Ok(CallToolResult {
|
||||
content: vec![Content::text("Result")],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
));
|
||||
|
||||
let spec = format_messages(&messages, &ImageFormat::OpenAi);
|
||||
|
||||
|
||||
@@ -0,0 +1,696 @@
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::{ProviderUsage, Usage};
|
||||
use anyhow::{anyhow, Error};
|
||||
use async_stream::try_stream;
|
||||
use chrono;
|
||||
use futures::Stream;
|
||||
use rmcp::model::{object, CallToolRequestParam, RawContent, Role, Tool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResponsesApiResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created_at: i64,
|
||||
pub status: String,
|
||||
pub model: String,
|
||||
pub output: Vec<ResponseOutputItem>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<ResponseReasoningInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<ResponseUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ResponseOutputItem {
|
||||
Reasoning {
|
||||
id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
summary: Option<Vec<String>>,
|
||||
},
|
||||
Message {
|
||||
id: String,
|
||||
status: String,
|
||||
role: String,
|
||||
content: Vec<ResponseContentBlock>,
|
||||
},
|
||||
FunctionCall {
|
||||
id: String,
|
||||
status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
call_id: Option<String>,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ResponseContentBlock {
|
||||
OutputText {
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
annotations: Option<Vec<Value>>,
|
||||
},
|
||||
ToolCall {
|
||||
id: String,
|
||||
name: String,
|
||||
input: Value,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResponseReasoningInfo {
|
||||
pub effort: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResponseUsage {
|
||||
pub input_tokens: i32,
|
||||
pub output_tokens: i32,
|
||||
pub total_tokens: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ResponsesStreamEvent {
|
||||
#[serde(rename = "response.created")]
|
||||
ResponseCreated {
|
||||
sequence_number: i32,
|
||||
response: ResponseMetadata,
|
||||
},
|
||||
#[serde(rename = "response.in_progress")]
|
||||
ResponseInProgress {
|
||||
sequence_number: i32,
|
||||
response: ResponseMetadata,
|
||||
},
|
||||
#[serde(rename = "response.output_item.added")]
|
||||
OutputItemAdded {
|
||||
sequence_number: i32,
|
||||
output_index: i32,
|
||||
item: ResponseOutputItemInfo,
|
||||
},
|
||||
#[serde(rename = "response.content_part.added")]
|
||||
ContentPartAdded {
|
||||
sequence_number: i32,
|
||||
item_id: String,
|
||||
output_index: i32,
|
||||
content_index: i32,
|
||||
part: ContentPart,
|
||||
},
|
||||
#[serde(rename = "response.output_text.delta")]
|
||||
OutputTextDelta {
|
||||
sequence_number: i32,
|
||||
item_id: String,
|
||||
output_index: i32,
|
||||
content_index: i32,
|
||||
delta: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
logprobs: Option<Vec<Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
obfuscation: Option<String>,
|
||||
},
|
||||
#[serde(rename = "response.output_item.done")]
|
||||
OutputItemDone {
|
||||
sequence_number: i32,
|
||||
output_index: i32,
|
||||
item: ResponseOutputItemInfo,
|
||||
},
|
||||
#[serde(rename = "response.content_part.done")]
|
||||
ContentPartDone {
|
||||
sequence_number: i32,
|
||||
item_id: String,
|
||||
output_index: i32,
|
||||
content_index: i32,
|
||||
part: ContentPart,
|
||||
},
|
||||
#[serde(rename = "response.output_text.done")]
|
||||
OutputTextDone {
|
||||
sequence_number: i32,
|
||||
item_id: String,
|
||||
output_index: i32,
|
||||
content_index: i32,
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
logprobs: Option<Vec<Value>>,
|
||||
},
|
||||
#[serde(rename = "response.completed")]
|
||||
ResponseCompleted {
|
||||
sequence_number: i32,
|
||||
response: ResponseMetadata,
|
||||
},
|
||||
#[serde(rename = "response.failed")]
|
||||
ResponseFailed { sequence_number: i32, error: Value },
|
||||
#[serde(rename = "response.function_call_arguments.delta")]
|
||||
FunctionCallArgumentsDelta {
|
||||
sequence_number: i32,
|
||||
item_id: String,
|
||||
output_index: i32,
|
||||
delta: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
obfuscation: Option<String>,
|
||||
},
|
||||
#[serde(rename = "response.function_call_arguments.done")]
|
||||
FunctionCallArgumentsDone {
|
||||
sequence_number: i32,
|
||||
item_id: String,
|
||||
output_index: i32,
|
||||
arguments: String,
|
||||
},
|
||||
#[serde(rename = "error")]
|
||||
Error { error: Value },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResponseMetadata {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created_at: i64,
|
||||
pub status: String,
|
||||
pub model: String,
|
||||
pub output: Vec<ResponseOutputItemInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<ResponseUsage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<ResponseReasoningInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ResponseOutputItemInfo {
|
||||
Reasoning {
|
||||
id: String,
|
||||
summary: Vec<String>,
|
||||
},
|
||||
Message {
|
||||
id: String,
|
||||
status: String,
|
||||
role: String,
|
||||
content: Vec<ContentPart>,
|
||||
},
|
||||
FunctionCall {
|
||||
id: String,
|
||||
status: String,
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ContentPart {
|
||||
OutputText {
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
annotations: Option<Vec<Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
logprobs: Option<Vec<Value>>,
|
||||
},
|
||||
ToolCall {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn add_conversation_history(input_items: &mut Vec<Value>, messages: &[Message]) {
|
||||
for message in messages.iter().filter(|m| m.is_agent_visible()) {
|
||||
let has_only_tool_content = message.content.iter().all(|c| {
|
||||
matches!(
|
||||
c,
|
||||
MessageContent::ToolRequest(_) | MessageContent::ToolResponse(_)
|
||||
)
|
||||
});
|
||||
|
||||
if has_only_tool_content {
|
||||
continue;
|
||||
}
|
||||
|
||||
if message.role != Role::User && message.role != Role::Assistant {
|
||||
continue;
|
||||
}
|
||||
|
||||
let role = match message.role {
|
||||
Role::User => "user",
|
||||
Role::Assistant => "assistant",
|
||||
};
|
||||
|
||||
let mut content_items = Vec::new();
|
||||
for content in &message.content {
|
||||
if let MessageContent::Text(text) = content {
|
||||
if !text.text.is_empty() {
|
||||
let content_type = if message.role == Role::Assistant {
|
||||
"output_text"
|
||||
} else {
|
||||
"input_text"
|
||||
};
|
||||
content_items.push(json!({
|
||||
"type": content_type,
|
||||
"text": text.text
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !content_items.is_empty() {
|
||||
input_items.push(json!({
|
||||
"role": role,
|
||||
"content": content_items
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_function_calls(input_items: &mut Vec<Value>, messages: &[Message]) {
|
||||
for message in messages.iter().filter(|m| m.is_agent_visible()) {
|
||||
if message.role == Role::Assistant {
|
||||
for content in &message.content {
|
||||
if let MessageContent::ToolRequest(request) = content {
|
||||
if let Ok(tool_call) = &request.tool_call {
|
||||
let arguments_str = tool_call
|
||||
.arguments
|
||||
.as_ref()
|
||||
.map(|args| {
|
||||
serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "{}".to_string());
|
||||
|
||||
tracing::debug!(
|
||||
"Replaying function_call with call_id: {}, name: {}",
|
||||
request.id,
|
||||
tool_call.name
|
||||
);
|
||||
input_items.push(json!({
|
||||
"type": "function_call",
|
||||
"call_id": request.id,
|
||||
"name": tool_call.name,
|
||||
"arguments": arguments_str
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_function_call_outputs(input_items: &mut Vec<Value>, messages: &[Message]) {
|
||||
for message in messages.iter().filter(|m| m.is_agent_visible()) {
|
||||
for content in &message.content {
|
||||
if let MessageContent::ToolResponse(response) = content {
|
||||
match &response.tool_result {
|
||||
Ok(contents) => {
|
||||
let text_content: Vec<String> = contents
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
if let RawContent::Text(t) = c.deref() {
|
||||
Some(t.text.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !text_content.is_empty() {
|
||||
tracing::debug!(
|
||||
"Sending function_call_output with call_id: {}",
|
||||
response.id
|
||||
);
|
||||
input_items.push(json!({
|
||||
"type": "function_call_output",
|
||||
"call_id": response.id,
|
||||
"output": text_content.join("\n")
|
||||
}));
|
||||
}
|
||||
}
|
||||
Err(error_data) => {
|
||||
// Handle error responses - must send them back to the API
|
||||
// to avoid "No tool output found" errors
|
||||
tracing::debug!(
|
||||
"Sending function_call_output error with call_id: {}",
|
||||
response.id
|
||||
);
|
||||
input_items.push(json!({
|
||||
"type": "function_call_output",
|
||||
"call_id": response.id,
|
||||
"output": format!("Error: {}", error_data.message)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_responses_request(
|
||||
model_config: &ModelConfig,
|
||||
system: &str,
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
) -> anyhow::Result<Value, Error> {
|
||||
let mut input_items = Vec::new();
|
||||
|
||||
if !system.is_empty() {
|
||||
input_items.push(json!({
|
||||
"role": "system",
|
||||
"content": [{
|
||||
"type": "input_text",
|
||||
"text": system
|
||||
}]
|
||||
}));
|
||||
}
|
||||
|
||||
add_conversation_history(&mut input_items, messages);
|
||||
add_function_calls(&mut input_items, messages);
|
||||
add_function_call_outputs(&mut input_items, messages);
|
||||
|
||||
let mut payload = json!({
|
||||
"model": model_config.model_name,
|
||||
"input": input_items,
|
||||
"store": false, // Don't store responses on server (we replay history ourselves)
|
||||
});
|
||||
|
||||
if !tools.is_empty() {
|
||||
let tools_spec: Vec<Value> = tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
json!({
|
||||
"type": "function",
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.input_schema,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
payload
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("tools".to_string(), json!(tools_spec));
|
||||
}
|
||||
|
||||
if let Some(temp) = model_config.temperature {
|
||||
payload
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("temperature".to_string(), json!(temp));
|
||||
}
|
||||
|
||||
if let Some(tokens) = model_config.max_tokens {
|
||||
payload
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("max_output_tokens".to_string(), json!(tokens));
|
||||
}
|
||||
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub fn responses_api_to_message(response: &ResponsesApiResponse) -> anyhow::Result<Message> {
|
||||
let mut content = Vec::new();
|
||||
|
||||
for item in &response.output {
|
||||
match item {
|
||||
ResponseOutputItem::Reasoning { .. } => {
|
||||
continue;
|
||||
}
|
||||
ResponseOutputItem::Message {
|
||||
content: msg_content,
|
||||
..
|
||||
} => {
|
||||
for block in msg_content {
|
||||
match block {
|
||||
ResponseContentBlock::OutputText { text, .. } => {
|
||||
if !text.is_empty() {
|
||||
content.push(MessageContent::text(text));
|
||||
}
|
||||
}
|
||||
ResponseContentBlock::ToolCall { id, name, input } => {
|
||||
content.push(MessageContent::tool_request(
|
||||
id.clone(),
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.clone().into(),
|
||||
arguments: Some(object(input.clone())),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseOutputItem::FunctionCall {
|
||||
id,
|
||||
name,
|
||||
arguments,
|
||||
..
|
||||
} => {
|
||||
tracing::debug!("Received FunctionCall with id: {}, name: {}", id, name);
|
||||
let parsed_args = if arguments.is_empty() {
|
||||
json!({})
|
||||
} else {
|
||||
serde_json::from_str(arguments).unwrap_or_else(|_| json!({}))
|
||||
};
|
||||
|
||||
content.push(MessageContent::tool_request(
|
||||
id.clone(),
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.clone().into(),
|
||||
arguments: Some(object(parsed_args)),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut message = Message::new(Role::Assistant, chrono::Utc::now().timestamp(), content);
|
||||
|
||||
message = message.with_id(response.id.clone());
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub fn get_responses_usage(response: &ResponsesApiResponse) -> Usage {
|
||||
response.usage.as_ref().map_or_else(Usage::default, |u| {
|
||||
Usage::new(
|
||||
Some(u.input_tokens),
|
||||
Some(u.output_tokens),
|
||||
Some(u.total_tokens),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn process_streaming_output_items(
|
||||
output_items: Vec<ResponseOutputItemInfo>,
|
||||
is_text_response: bool,
|
||||
) -> Vec<MessageContent> {
|
||||
let mut content = Vec::new();
|
||||
|
||||
for item in output_items {
|
||||
match item {
|
||||
ResponseOutputItemInfo::Reasoning { .. } => {
|
||||
// Skip reasoning items
|
||||
}
|
||||
ResponseOutputItemInfo::Message { content: parts, .. } => {
|
||||
for part in parts {
|
||||
match part {
|
||||
ContentPart::OutputText { text, .. } => {
|
||||
if !text.is_empty() && !is_text_response {
|
||||
content.push(MessageContent::text(&text));
|
||||
}
|
||||
}
|
||||
ContentPart::ToolCall {
|
||||
id,
|
||||
name,
|
||||
arguments,
|
||||
} => {
|
||||
let parsed_args = if arguments.is_empty() {
|
||||
json!({})
|
||||
} else {
|
||||
serde_json::from_str(&arguments).unwrap_or_else(|_| json!({}))
|
||||
};
|
||||
|
||||
content.push(MessageContent::tool_request(
|
||||
id,
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(parsed_args)),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseOutputItemInfo::FunctionCall {
|
||||
call_id,
|
||||
name,
|
||||
arguments,
|
||||
..
|
||||
} => {
|
||||
let parsed_args = if arguments.is_empty() {
|
||||
json!({})
|
||||
} else {
|
||||
serde_json::from_str(&arguments).unwrap_or_else(|_| json!({}))
|
||||
};
|
||||
|
||||
content.push(MessageContent::tool_request(
|
||||
call_id,
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(parsed_args)),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
pub fn responses_api_to_streaming_message<S>(
|
||||
mut stream: S,
|
||||
) -> impl Stream<Item = anyhow::Result<(Option<Message>, Option<ProviderUsage>)>> + 'static
|
||||
where
|
||||
S: Stream<Item = anyhow::Result<String>> + Unpin + Send + 'static,
|
||||
{
|
||||
try_stream! {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut accumulated_text = String::new();
|
||||
let mut response_id: Option<String> = None;
|
||||
let mut model_name: Option<String> = None;
|
||||
let mut final_usage: Option<ProviderUsage> = None;
|
||||
let mut output_items: Vec<ResponseOutputItemInfo> = Vec::new();
|
||||
let mut is_text_response = false;
|
||||
|
||||
'outer: while let Some(response) = stream.next().await {
|
||||
let response_str = response?;
|
||||
|
||||
// Skip empty lines
|
||||
if response_str.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse SSE format: "event: <type>\ndata: <json>"
|
||||
// For now, we only care about the data line
|
||||
let data_line = if response_str.starts_with("data: ") {
|
||||
response_str.strip_prefix("data: ").unwrap()
|
||||
} else if response_str.starts_with("event: ") {
|
||||
// Skip event type lines
|
||||
continue;
|
||||
} else {
|
||||
// Try to parse as-is in case there's no prefix
|
||||
&response_str
|
||||
};
|
||||
|
||||
if data_line == "[DONE]" {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
let event: ResponsesStreamEvent = serde_json::from_str(data_line)
|
||||
.map_err(|e| anyhow!("Failed to parse Responses stream event: {}: {:?}", e, data_line))?;
|
||||
|
||||
match event {
|
||||
ResponsesStreamEvent::ResponseCreated { response, .. } |
|
||||
ResponsesStreamEvent::ResponseInProgress { response, .. } => {
|
||||
response_id = Some(response.id);
|
||||
model_name = Some(response.model);
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::OutputTextDelta { delta, .. } => {
|
||||
is_text_response = true;
|
||||
accumulated_text.push_str(&delta);
|
||||
|
||||
// Yield incremental text updates for true streaming
|
||||
let mut content = Vec::new();
|
||||
if !delta.is_empty() {
|
||||
content.push(MessageContent::text(&delta));
|
||||
}
|
||||
let mut msg = Message::new(Role::Assistant, chrono::Utc::now().timestamp(), content);
|
||||
|
||||
// Add ID so desktop client knows these deltas are part of the same message
|
||||
if let Some(id) = &response_id {
|
||||
msg = msg.with_id(id.clone());
|
||||
}
|
||||
|
||||
yield (Some(msg), None);
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::OutputItemDone { item, .. } => {
|
||||
output_items.push(item);
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::OutputTextDone { .. } => {
|
||||
// Text is already complete from deltas, this is just a summary event
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::ResponseCompleted { response, .. } => {
|
||||
let model = model_name.as_ref().unwrap_or(&response.model);
|
||||
let usage = response.usage.as_ref().map_or_else(
|
||||
Usage::default,
|
||||
|u| Usage::new(
|
||||
Some(u.input_tokens),
|
||||
Some(u.output_tokens),
|
||||
Some(u.total_tokens),
|
||||
),
|
||||
);
|
||||
final_usage = Some(ProviderUsage {
|
||||
usage,
|
||||
model: model.clone(),
|
||||
});
|
||||
|
||||
// For complete output, use the response output items
|
||||
if !response.output.is_empty() {
|
||||
output_items = response.output;
|
||||
}
|
||||
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::FunctionCallArgumentsDelta { .. } => {
|
||||
// Function call arguments are being streamed, but we'll get the complete
|
||||
// arguments in the OutputItemDone event, so we can ignore deltas for now
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::FunctionCallArgumentsDone { .. } => {
|
||||
// Arguments are complete, will be in the OutputItemDone event
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::ResponseFailed { error, .. } => {
|
||||
Err(anyhow!("Responses API failed: {:?}", error))?;
|
||||
}
|
||||
|
||||
ResponsesStreamEvent::Error { error } => {
|
||||
Err(anyhow!("Responses API error: {:?}", error))?;
|
||||
}
|
||||
|
||||
_ => {
|
||||
// Ignore other event types (OutputItemAdded, ContentPartAdded, ContentPartDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process final output items and yield usage data
|
||||
let content = process_streaming_output_items(output_items, is_text_response);
|
||||
|
||||
if !content.is_empty() {
|
||||
let mut message = Message::new(Role::Assistant, chrono::Utc::now().timestamp(), content);
|
||||
if let Some(id) = response_id {
|
||||
message = message.with_id(id);
|
||||
}
|
||||
yield (Some(message), final_usage);
|
||||
} else if let Some(usage) = final_usage {
|
||||
yield (None, Some(usage));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
MessageContent::ToolResponse(tool_response) => {
|
||||
if let Ok(result) = &tool_response.tool_result {
|
||||
let text = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|c| c.as_text().map(|t| t.text.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -215,9 +215,9 @@ impl LeadWorkerProvider {
|
||||
if let Err(tool_error) = &tool_response.tool_result {
|
||||
failure_indicators += 1;
|
||||
tracing::debug!("Tool execution failure detected: {:?}", tool_error);
|
||||
} else if let Ok(contents) = &tool_response.tool_result {
|
||||
} else if let Ok(result) = &tool_response.tool_result {
|
||||
// Check tool output for error indicators
|
||||
if self.contains_error_indicators(contents) {
|
||||
if self.contains_error_indicators(&result.content) {
|
||||
failure_indicators += 1;
|
||||
tracing::debug!("Tool output contains error indicators");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod azure;
|
||||
pub mod azureauth;
|
||||
pub mod base;
|
||||
pub mod bedrock;
|
||||
pub mod canonical;
|
||||
pub mod claude_code;
|
||||
pub mod cursor_agent;
|
||||
pub mod databricks;
|
||||
|
||||
@@ -15,7 +15,13 @@ use super::api_client::{ApiClient, AuthMethod};
|
||||
use super::base::{ConfigKey, ModelInfo, Provider, ProviderMetadata, ProviderUsage, Usage};
|
||||
use super::embedding::{EmbeddingCapable, EmbeddingRequest, EmbeddingResponse};
|
||||
use super::errors::ProviderError;
|
||||
use super::formats::openai::{create_request, get_usage, response_to_message};
|
||||
use super::formats::openai::{
|
||||
create_request, get_usage, response_to_message, response_to_streaming_message,
|
||||
};
|
||||
use super::formats::openai_responses::{
|
||||
create_responses_request, get_responses_usage, responses_api_to_message,
|
||||
responses_api_to_streaming_message, ResponsesApiResponse,
|
||||
};
|
||||
use super::retry::ProviderRetry;
|
||||
use super::utils::{
|
||||
get_model, handle_response_openai_compat, handle_status_openai_compat, ImageFormat,
|
||||
@@ -25,7 +31,6 @@ use crate::conversation::message::Message;
|
||||
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::MessageStream;
|
||||
use crate::providers::formats::openai::response_to_streaming_message;
|
||||
use crate::providers::utils::RequestLog;
|
||||
use rmcp::model::Tool;
|
||||
|
||||
@@ -41,6 +46,8 @@ pub const OPEN_AI_KNOWN_MODELS: &[(&str, usize)] = &[
|
||||
("gpt-3.5-turbo", 16_385),
|
||||
("gpt-4-turbo", 128_000),
|
||||
("o4-mini", 128_000),
|
||||
("gpt-5.1-codex", 400_000),
|
||||
("gpt-5-codex", 400_000),
|
||||
];
|
||||
|
||||
pub const OPEN_AI_DOC_URL: &str = "https://platform.openai.com/docs/models";
|
||||
@@ -184,6 +191,10 @@ impl OpenAiProvider {
|
||||
})
|
||||
}
|
||||
|
||||
fn uses_responses_api(model_name: &str) -> bool {
|
||||
model_name.starts_with("gpt-5-codex") || model_name.starts_with("gpt-5.1-codex")
|
||||
}
|
||||
|
||||
async fn post(&self, payload: &Value) -> Result<Value, ProviderError> {
|
||||
let response = self
|
||||
.api_client
|
||||
@@ -191,6 +202,14 @@ impl OpenAiProvider {
|
||||
.await?;
|
||||
handle_response_openai_compat(response).await
|
||||
}
|
||||
|
||||
async fn post_responses(&self, payload: &Value) -> Result<Value, ProviderError> {
|
||||
let response = self
|
||||
.api_client
|
||||
.response_post("v1/responses", payload)
|
||||
.await?;
|
||||
handle_response_openai_compat(response).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -238,31 +257,62 @@ impl Provider for OpenAiProvider {
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
) -> Result<(Message, ProviderUsage), ProviderError> {
|
||||
let payload = create_request(model_config, system, messages, tools, &ImageFormat::OpenAi)?;
|
||||
if Self::uses_responses_api(&model_config.model_name) {
|
||||
let payload = create_responses_request(model_config, system, messages, tools)?;
|
||||
let mut log = RequestLog::start(&self.model, &payload)?;
|
||||
|
||||
let mut log = RequestLog::start(&self.model, &payload)?;
|
||||
let json_response = self
|
||||
.with_retry(|| async {
|
||||
let payload_clone = payload.clone();
|
||||
self.post(&payload_clone).await
|
||||
})
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
let _ = log.error(e);
|
||||
})?;
|
||||
let json_response = self
|
||||
.with_retry(|| async {
|
||||
let payload_clone = payload.clone();
|
||||
self.post_responses(&payload_clone).await
|
||||
})
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
let _ = log.error(e);
|
||||
})?;
|
||||
|
||||
let message = response_to_message(&json_response)?;
|
||||
let usage = json_response
|
||||
.get("usage")
|
||||
.map(get_usage)
|
||||
.unwrap_or_else(|| {
|
||||
tracing::debug!("Failed to get usage data");
|
||||
Usage::default()
|
||||
});
|
||||
let responses_api_response: ResponsesApiResponse =
|
||||
serde_json::from_value(json_response.clone()).map_err(|e| {
|
||||
ProviderError::ExecutionError(format!(
|
||||
"Failed to parse responses API response: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let model = get_model(&json_response);
|
||||
log.write(&json_response, Some(&usage))?;
|
||||
Ok((message, ProviderUsage::new(model, usage)))
|
||||
let message = responses_api_to_message(&responses_api_response)?;
|
||||
let usage = get_responses_usage(&responses_api_response);
|
||||
let model = responses_api_response.model.clone();
|
||||
|
||||
log.write(&json_response, Some(&usage))?;
|
||||
Ok((message, ProviderUsage::new(model, usage)))
|
||||
} else {
|
||||
let payload =
|
||||
create_request(model_config, system, messages, tools, &ImageFormat::OpenAi)?;
|
||||
|
||||
let mut log = RequestLog::start(&self.model, &payload)?;
|
||||
let json_response = self
|
||||
.with_retry(|| async {
|
||||
let payload_clone = payload.clone();
|
||||
self.post(&payload_clone).await
|
||||
})
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
let _ = log.error(e);
|
||||
})?;
|
||||
|
||||
let message = response_to_message(&json_response)?;
|
||||
let usage = json_response
|
||||
.get("usage")
|
||||
.map(get_usage)
|
||||
.unwrap_or_else(|| {
|
||||
tracing::debug!("Failed to get usage data");
|
||||
Usage::default()
|
||||
});
|
||||
|
||||
let model = get_model(&json_response);
|
||||
log.write(&json_response, Some(&usage))?;
|
||||
Ok((message, ProviderUsage::new(model, usage)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_supported_models(&self) -> Result<Option<Vec<String>>, ProviderError> {
|
||||
@@ -319,40 +369,77 @@ impl Provider for OpenAiProvider {
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
) -> Result<MessageStream, ProviderError> {
|
||||
let mut payload =
|
||||
create_request(&self.model, system, messages, tools, &ImageFormat::OpenAi)?;
|
||||
payload["stream"] = serde_json::Value::Bool(true);
|
||||
payload["stream_options"] = json!({
|
||||
"include_usage": true,
|
||||
});
|
||||
let mut log = RequestLog::start(&self.model, &payload)?;
|
||||
if Self::uses_responses_api(&self.model.model_name) {
|
||||
let mut payload = create_responses_request(&self.model, system, messages, tools)?;
|
||||
payload["stream"] = serde_json::Value::Bool(true);
|
||||
|
||||
let response = self
|
||||
.with_retry(|| async {
|
||||
let resp = self
|
||||
.api_client
|
||||
.response_post(&self.base_path, &payload)
|
||||
.await?;
|
||||
handle_status_openai_compat(resp).await
|
||||
})
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
let _ = log.error(e);
|
||||
})?;
|
||||
let stream = response.bytes_stream().map_err(io::Error::other);
|
||||
let mut log = RequestLog::start(&self.model, &payload)?;
|
||||
|
||||
Ok(Box::pin(try_stream! {
|
||||
let stream_reader = StreamReader::new(stream);
|
||||
let framed = FramedRead::new(stream_reader, LinesCodec::new()).map_err(anyhow::Error::from);
|
||||
let response = self
|
||||
.with_retry(|| async {
|
||||
let payload_clone = payload.clone();
|
||||
let resp = self
|
||||
.api_client
|
||||
.response_post("v1/responses", &payload_clone)
|
||||
.await?;
|
||||
handle_status_openai_compat(resp).await
|
||||
})
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
let _ = log.error(e);
|
||||
})?;
|
||||
|
||||
let message_stream = response_to_streaming_message(framed);
|
||||
pin!(message_stream);
|
||||
while let Some(message) = message_stream.next().await {
|
||||
let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?;
|
||||
log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?;
|
||||
yield (message, usage);
|
||||
}
|
||||
}))
|
||||
let stream = response.bytes_stream().map_err(io::Error::other);
|
||||
|
||||
Ok(Box::pin(try_stream! {
|
||||
let stream_reader = StreamReader::new(stream);
|
||||
let framed = FramedRead::new(stream_reader, LinesCodec::new()).map_err(anyhow::Error::from);
|
||||
|
||||
let message_stream = responses_api_to_streaming_message(framed);
|
||||
pin!(message_stream);
|
||||
while let Some(message) = message_stream.next().await {
|
||||
let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?;
|
||||
log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?;
|
||||
yield (message, usage);
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
let mut payload =
|
||||
create_request(&self.model, system, messages, tools, &ImageFormat::OpenAi)?;
|
||||
payload["stream"] = serde_json::Value::Bool(true);
|
||||
payload["stream_options"] = json!({
|
||||
"include_usage": true,
|
||||
});
|
||||
let mut log = RequestLog::start(&self.model, &payload)?;
|
||||
|
||||
let response = self
|
||||
.with_retry(|| async {
|
||||
let resp = self
|
||||
.api_client
|
||||
.response_post(&self.base_path, &payload)
|
||||
.await?;
|
||||
handle_status_openai_compat(resp).await
|
||||
})
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
let _ = log.error(e);
|
||||
})?;
|
||||
|
||||
let stream = response.bytes_stream().map_err(io::Error::other);
|
||||
|
||||
Ok(Box::pin(try_stream! {
|
||||
let stream_reader = StreamReader::new(stream);
|
||||
let framed = FramedRead::new(stream_reader, LinesCodec::new()).map_err(anyhow::Error::from);
|
||||
|
||||
let message_stream = response_to_streaming_message(framed);
|
||||
pin!(message_stream);
|
||||
while let Some(message) = message_stream.next().await {
|
||||
let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?;
|
||||
log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?;
|
||||
yield (message, usage);
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -343,8 +343,9 @@ pub fn convert_tool_messages_to_text(messages: &[Message]) -> Conversation {
|
||||
has_tool_content = true;
|
||||
// Convert tool response to text format
|
||||
let text = match &res.tool_result {
|
||||
Ok(contents) => {
|
||||
let text_contents: Vec<String> = contents
|
||||
Ok(result) => {
|
||||
let text_contents: Vec<String> = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|c| match c.deref() {
|
||||
RawContent::Text(t) => Some(t.text.clone()),
|
||||
|
||||
@@ -36,7 +36,7 @@ impl SecurityManager {
|
||||
let config = Config::global();
|
||||
|
||||
config
|
||||
.get_param::<bool>("security_prompt_enabled")
|
||||
.get_param::<bool>("SECURITY_PROMPT_ENABLED")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ impl PromptInjectionScanner {
|
||||
use crate::config::Config;
|
||||
let config = Config::global();
|
||||
|
||||
if let Ok(threshold) = config.get_param::<f64>("security_prompt_threshold") {
|
||||
if let Ok(threshold) = config.get_param::<f64>("SECURITY_PROMPT_THRESHOLD") {
|
||||
return threshold as f32;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,8 +120,8 @@ pub struct SessionUpdateBuilder {
|
||||
#[derive(Serialize, ToSchema, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SessionInsights {
|
||||
total_sessions: usize,
|
||||
total_tokens: i64,
|
||||
pub total_sessions: usize,
|
||||
pub total_tokens: i64,
|
||||
}
|
||||
|
||||
impl SessionUpdateBuilder {
|
||||
@@ -887,6 +887,7 @@ impl SessionStorage {
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
crate::posthog::emit_session_started();
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
|
||||
@@ -118,10 +118,10 @@ mod tests {
|
||||
let tool_result = result.result.await;
|
||||
assert!(tool_result.is_ok());
|
||||
let contents = tool_result.unwrap();
|
||||
assert!(!contents.is_empty());
|
||||
assert!(!contents.content.is_empty());
|
||||
|
||||
// Parse the returned JSON to verify task creation
|
||||
if let Some(text_content) = contents.first().and_then(|c| c.as_text()) {
|
||||
if let Some(text_content) = contents.content.first().and_then(|c| c.as_text()) {
|
||||
let task_payload: serde_json::Value = serde_json::from_str(&text_content.text).unwrap();
|
||||
assert!(task_payload.get("task_ids").is_some());
|
||||
let task_ids = task_payload.get("task_ids").unwrap().as_array().unwrap();
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::{env, fs};
|
||||
|
||||
use rmcp::model::{CallToolRequestParam, Content, Tool};
|
||||
use rmcp::model::{CallToolRequestParam, CallToolResult, Tool};
|
||||
use rmcp::object;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -108,7 +108,7 @@ fn build_and_get_binary_path() -> PathBuf {
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.expect("failed to parase binary path")
|
||||
.expect("failed to parse binary path")
|
||||
}
|
||||
|
||||
static REPLAY_BINARY_PATH: Lazy<PathBuf> = Lazy::new(build_and_get_binary_path);
|
||||
@@ -284,7 +284,7 @@ async fn test_replayed_session(
|
||||
serde_json::to_writer_pretty(File::create(results_path)?, &results)?
|
||||
}
|
||||
TestMode::Playback => assert_eq!(
|
||||
serde_json::from_reader::<_, Vec<Vec<Content>>>(File::open(results_path)?)?,
|
||||
serde_json::from_reader::<_, Vec<CallToolResult>>(File::open(results_path)?)?,
|
||||
results
|
||||
),
|
||||
};
|
||||
|
||||
+13
-15
File diff suppressed because one or more lines are too long
+118
-103
@@ -1,111 +1,126 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"type": "resource",
|
||||
"resource": {
|
||||
"uri": "file:///tmp/goose_test/goose.txt",
|
||||
"mimeType": "text",
|
||||
"text": "# goose\n"
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "resource",
|
||||
"resource": {
|
||||
"uri": "file:///tmp/goose_test/goose.txt",
|
||||
"mimeType": "text",
|
||||
"text": "# goose\n"
|
||||
},
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
}
|
||||
},
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
{
|
||||
"type": "text",
|
||||
"text": "### /tmp/goose_test/goose.txt\n```\n1: # goose\n```\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "### /tmp/goose_test/goose.txt\n```\n1: # goose\n```\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.0
|
||||
],
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose (modified by test)\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "```\n# goose (modified by test)\n```\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose (modified by test)\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
],
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "# goose (modified by test)\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "# goose (modified by test)\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "```\n# goose (modified by test)\n```\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.2
|
||||
],
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "```\n# goose\n```\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "# goose (modified by test)\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
],
|
||||
"isError": false
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Available windows:\nMenubar",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Available windows:\nMenubar",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "# goose (modified by test)\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The file /tmp/goose_test/goose.txt has been edited, and the section now reads:\n```\n# goose\n```\n\nReview the changes above for errors. Undo and edit the file again if necessary!\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "```\n# goose\n```\n",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.2
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Available windows:\n\nItem-0\nbb3cc23c-6950-4e96-8b40-850e09f46934\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nBattery\nWiFi\nItem-0\nBentoBox\nSiri\nClock\nMenubar\nDock\njust record-mcp-tests",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"assistant"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Available windows:\n\nItem-0\nbb3cc23c-6950-4e96-8b40-850e09f46934\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nItem-0\nBattery\nWiFi\nItem-0\nBentoBox\nSiri\nClock\nMenubar\nDock\njust record-mcp-tests",
|
||||
"annotations": {
|
||||
"audience": [
|
||||
"user"
|
||||
],
|
||||
"priority": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,12 @@
|
||||
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"elicitation": {}, "sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
|
||||
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{},"elicitation":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
|
||||
STDERR: time=2025-12-11T17:58:47.636-05:00 level=INFO msg="starting server" version=0.24.1 host="" dynamicToolsets=false readOnly=false lockdownEnabled=false
|
||||
STDERR: GitHub MCP Server running on stdio
|
||||
STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"logging":{},"prompts":{},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"github-mcp-server","version":"version"}}}
|
||||
STDERR: time=2025-12-11T17:58:47.640-05:00 level=INFO msg="server run start"
|
||||
STDERR: time=2025-12-11T17:58:47.640-05:00 level=INFO msg="server connecting"
|
||||
STDERR: time=2025-12-11T17:58:47.640-05:00 level=INFO msg="server session connected" session_id=""
|
||||
STDOUT: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"instructions":"The GitHub MCP Server provides tools to interact with GitHub platform.\n\nTool selection guidance:\n\t1. Use 'list_*' tools for broad, simple retrieval and pagination of all items of a type (e.g., all issues, all PRs, all branches) with basic filtering.\n\t2. Use 'search_*' tools for targeted queries with specific criteria, keywords, or complex filters (e.g., issues with certain text, PRs by author, code containing functions).\n\nContext management:\n\t1. Use pagination whenever possible with batches of 5-10 items.\n\t2. Use minimal_output parameter set to true if the full information is not needed to accomplish a task.\n\nTool usage guidance:\n\t1. For 'search_*' tools: Use separate 'sort' and 'order' parameters if available for sorting results - do not include 'sort:' syntax in query strings. Query strings should contain only search criteria (e.g., 'org:google language:python'), not sorting instructions. Always call 'get_me' first to understand current user permissions and context. ## Issues\n\nCheck 'list_issue_types' first for organizations to use proper issue types. Use 'search_issues' before creating new issues to avoid duplicates. Always set 'state_reason' when closing issues. ## Pull Requests\n\nPR review workflow: Always use 'pull_request_review_write' with method 'create' to create a pending review, then 'add_comment_to_pending_review' to add comments, and finally 'pull_request_review_write' with method 'submit_pending' to submit the review for complex reviews with line-specific comments.\n\nBefore creating a pull request, search for pull request templates in the repository. Template files are called pull_request_template.md or they're located in '.github/PULL_REQUEST_TEMPLATE' directory. Use the template content to structure the PR description and then call create_pull_request tool.","protocolVersion":"2025-03-26","serverInfo":{"name":"github-mcp-server","title":"GitHub MCP Server","version":"0.24.1"}}}
|
||||
STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"}
|
||||
STDERR: time=2025-12-11T17:58:47.642-05:00 level=INFO msg="session initialized"
|
||||
STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"get_file_contents","arguments":{"owner":"block","path":"README.md","repo":"goose","sha":"ab62b863c1666232a67048b6c4e10007a2a5b83c"}}}
|
||||
STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"successfully downloaded text file"},{"type":"resource","resource":{"uri":"repo://block/goose/sha/ab62b863c1666232a67048b6c4e10007a2a5b83c/contents/README.md","mimeType":"text/plain; charset=utf-8","text":"\u003cdiv align=\"center\"\u003e\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://opensource.org/licenses/Apache-2.0\"\u003e\n \u003cimg src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://discord.gg/7GaTvbDwga\"\u003e\n \u003cimg src=\"https://img.shields.io/discord/1287729918100246654?logo=discord\u0026logoColor=white\u0026label=Join+Us\u0026color=blueviolet\" alt=\"Discord\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://github.com/block/goose/actions/workflows/ci.yml\"\u003e\n \u003cimg src=\"https://img.shields.io/github/actions/workflow/status/block/goose/ci.yml?branch=main\" alt=\"CI\"\u003e\n \u003c/a\u003e\n\u003c/p\u003e\n\u003c/div\u003e\n\ngoose 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_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# Goose Around with Us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@blockopensource)\n- [LinkedIn](https://www.linkedin.com/company/block-opensource)\n- [Twitter/X](https://x.com/blockopensource)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n"}}]}}
|
||||
STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"successfully downloaded text file (SHA: de9bdde7f260549bf3a083651842f30ab29cf4e9)"},{"type":"resource","resource":{"uri":"repo://block/goose/sha/ab62b863c1666232a67048b6c4e10007a2a5b83c/contents/README.md","mimeType":"text/plain; charset=utf-8","text":"\u003cdiv align=\"center\"\u003e\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://opensource.org/licenses/Apache-2.0\"\u003e\n \u003cimg src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://discord.gg/7GaTvbDwga\"\u003e\n \u003cimg src=\"https://img.shields.io/discord/1287729918100246654?logo=discord\u0026logoColor=white\u0026label=Join+Us\u0026color=blueviolet\" alt=\"Discord\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://github.com/block/goose/actions/workflows/ci.yml\"\u003e\n \u003cimg src=\"https://img.shields.io/github/actions/workflow/status/block/goose/ci.yml?branch=main\" alt=\"CI\"\u003e\n \u003c/a\u003e\n\u003c/p\u003e\n\u003c/div\u003e\n\ngoose 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_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n[](https://youtu.be/D-DpDunrbpo)\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# a little goose humor 🦢\n\n\u003e Why did the developer choose goose as their AI agent?\n\u003e \n\u003e Because it always helps them \"migrate\" their code to production! 🚀\n\n# goose around with us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@goose-oss)\n- [LinkedIn](https://www.linkedin.com/company/goose-oss)\n- [Twitter/X](https://x.com/goose_oss)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n"}}]}}
|
||||
STDERR: time=2025-12-11T17:58:48.133-05:00 level=INFO msg="server session disconnected" session_id=""
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "successfully downloaded text file"
|
||||
},
|
||||
{
|
||||
"type": "resource",
|
||||
"resource": {
|
||||
"uri": "repo://block/goose/sha/ab62b863c1666232a67048b6c4e10007a2a5b83c/contents/README.md",
|
||||
"mimeType": "text/plain; charset=utf-8",
|
||||
"text": "<div align=\"center\">\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n<p align=\"center\">\n <a href=\"https://opensource.org/licenses/Apache-2.0\">\n <img src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\">\n </a>\n <a href=\"https://discord.gg/7GaTvbDwga\">\n <img src=\"https://img.shields.io/discord/1287729918100246654?logo=discord&logoColor=white&label=Join+Us&color=blueviolet\" alt=\"Discord\">\n </a>\n <a href=\"https://github.com/block/goose/actions/workflows/ci.yml\">\n <img src=\"https://img.shields.io/github/actions/workflow/status/block/goose/ci.yml?branch=main\" alt=\"CI\">\n </a>\n</p>\n</div>\n\ngoose 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_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# Goose Around with Us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@blockopensource)\n- [LinkedIn](https://www.linkedin.com/company/block-opensource)\n- [Twitter/X](https://x.com/blockopensource)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n"
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "successfully downloaded text file (SHA: de9bdde7f260549bf3a083651842f30ab29cf4e9)"
|
||||
},
|
||||
{
|
||||
"type": "resource",
|
||||
"resource": {
|
||||
"uri": "repo://block/goose/sha/ab62b863c1666232a67048b6c4e10007a2a5b83c/contents/README.md",
|
||||
"mimeType": "text/plain; charset=utf-8",
|
||||
"text": "<div align=\"center\">\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n<p align=\"center\">\n <a href=\"https://opensource.org/licenses/Apache-2.0\">\n <img src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\">\n </a>\n <a href=\"https://discord.gg/7GaTvbDwga\">\n <img src=\"https://img.shields.io/discord/1287729918100246654?logo=discord&logoColor=white&label=Join+Us&color=blueviolet\" alt=\"Discord\">\n </a>\n <a href=\"https://github.com/block/goose/actions/workflows/ci.yml\">\n <img src=\"https://img.shields.io/github/actions/workflow/status/block/goose/ci.yml?branch=main\" alt=\"CI\">\n </a>\n</p>\n</div>\n\ngoose 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_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n[](https://youtu.be/D-DpDunrbpo)\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# a little goose humor 🦢\n\n> Why did the developer choose goose as their AI agent?\n> \n> Because it always helps them \"migrate\" their code to production! 🚀\n\n# goose around with us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@goose-oss)\n- [LinkedIn](https://www.linkedin.com/company/goose-oss)\n- [Twitter/X](https://x.com/goose_oss)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,10 +1,10 @@
|
||||
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"elicitation": {}, "sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
|
||||
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{},"elicitation":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
|
||||
STDERR: Starting default (STDIO) server...
|
||||
STDERR: Starting logs update interval
|
||||
STDOUT: {"result":{"protocolVersion":"2025-03-26","capabilities":{"prompts":{},"resources":{"subscribe":true},"tools":{},"logging":{},"completions":{}},"serverInfo":{"name":"example-servers/everything","title":"Everything Example Server","version":"1.0.0"},"instructions":"Testing and demonstration server for MCP protocol features.\n\n## Resources\n\nResources 1-100 follow pattern: even IDs contain text, odd IDs contain binary data. Resources are paginated at 10 items per page with cursor-based navigation.\n\n## Key dependencies\n\nProgress notifications require `_meta.progressToken` in tool calls. Resource subscriptions generate updates every 10 seconds.\n\n## Performance characteristics\n\nServer generates automatic log messages every 20 seconds (filtered by current log level), stderr notifications every 30 seconds, and resource update notifications every 10 seconds for active subscriptions. Log level changes affect message filtering in real-time.\n\n## Multi-modal testing\n\n`complex_prompt` includes both text arguments and image content for testing client multi-modal handling. `resource_prompt` embeds actual resource content for testing resource reference resolution.\n\nArgument completion is available for prompt parameters and resource IDs. Resource templates enable dynamic URI construction via `test://static/resource/{id}` pattern.\n\n## Easter egg\n\nIf asked about server instructions, respond with \"🎉 Server instructions are working! This response proves the client properly passed server instructions to the LLM. This demonstrates MCP's instructions feature in action.\"\n"},"jsonrpc":"2.0","id":0}
|
||||
STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"}
|
||||
STDOUT: {"method":"notifications/message","params":{"level":"info","logger":"everything-server","data":"Client does not support MCP roots protocol"},"jsonrpc":"2.0"}
|
||||
STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"echo","arguments":{"message":"Hello, world!"}}}
|
||||
STDOUT: {"method":"notifications/message","params":{"level":"info","logger":"everything-server","data":"Client does not support MCP roots protocol"},"jsonrpc":"2.0"}
|
||||
STDOUT: {"result":{"content":[{"type":"text","text":"Echo: Hello, world!"}]},"jsonrpc":"2.0","id":1}
|
||||
STDIN: {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{"progressToken":1},"name":"add","arguments":{"a":1,"b":2}}}
|
||||
STDOUT: {"result":{"content":[{"type":"text","text":"The sum of 1 and 2 is 3."}]},"jsonrpc":"2.0","id":2}
|
||||
@@ -21,5 +21,5 @@ STDIN: {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"_meta":{"progres
|
||||
STDOUT: {"method":"sampling/createMessage","params":{"messages":[{"role":"user","content":{"type":"text","text":"Resource sampleLLM context: Please provide a quote from The Great Gatsby"}}],"systemPrompt":"You are a helpful test server.","maxTokens":100,"temperature":0.7,"includeContext":"thisServer"},"jsonrpc":"2.0","id":0}
|
||||
STDIN: {"jsonrpc":"2.0","id":0,"result":{"model":"mock","stopReason":"endTurn","role":"assistant","content":{"type":"text","text":"\"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"}}}
|
||||
STDOUT: {"result":{"content":[{"type":"text","text":"LLM sampling result: \"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"}]},"jsonrpc":"2.0","id":5}
|
||||
STDOUT: {"method":"notifications/message","params":{"level":"error","data":"Error-level message"},"jsonrpc":"2.0"}
|
||||
STDERR: node:events:486
|
||||
STDOUT: {"method":"notifications/message","params":{"level":"critical","data":"Critical-level message"},"jsonrpc":"2.0"}
|
||||
STDERR: node:events:485
|
||||
|
||||
+44
-29
@@ -1,32 +1,47 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Echo: Hello, world!"
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Echo: Hello, world!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The sum of 1 and 2 is 3."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Long running operation completed. Duration: 1 seconds, Steps: 5."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "{\"temperature\":22.5,\"conditions\":\"Partly cloudy\",\"humidity\":65}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"conditions": "Partly cloudy",
|
||||
"humidity": 65,
|
||||
"temperature": 22.5
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The sum of 1 and 2 is 3."
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Long running operation completed. Duration: 1 seconds, Steps: 5."
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "{\"temperature\":22.5,\"conditions\":\"Partly cloudy\",\"humidity\":65}"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "LLM sampling result: \"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "LLM sampling result: \"So we beat on, boats against the current, borne back ceaselessly into the past.\" — F. Scott Fitzgerald, The Great Gatsby (1925)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"elicitation": {}, "sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
|
||||
STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"experimental":{},"prompts":{"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"mcp-fetch","version":"1.19.0"}}}
|
||||
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{},"elicitation":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
|
||||
STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"experimental":{},"prompts":{"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"mcp-fetch","version":"1.23.3"}}}
|
||||
STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"}
|
||||
STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"fetch","arguments":{"url":"https://example.com"}}}
|
||||
STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Failed to fetch robots.txt https://example.com/robots.txt due to a connection issue"}],"isError":true}}
|
||||
STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Contents of https://example.com/:\nThis domain is for use in documentation examples without needing permission. Avoid use in operations.\n\n[Learn more](https://iana.org/domains/example)"}],"isError":false}}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Failed to fetch robots.txt https://example.com/robots.txt due to a connection issue"
|
||||
}
|
||||
]
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Contents of https://example.com/:\nThis domain is for use in documentation examples without needing permission. Avoid use in operations.\n\n[Learn more](https://iana.org/domains/example)"
|
||||
}
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
]
|
||||
@@ -166,8 +166,9 @@ impl ProviderTester {
|
||||
|
||||
let weather = Message::user().with_tool_response(
|
||||
id,
|
||||
Ok(vec![Content::text(
|
||||
"
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(
|
||||
"
|
||||
50°F°C
|
||||
Precipitation: 0%
|
||||
Humidity: 84%
|
||||
@@ -175,7 +176,11 @@ impl ProviderTester {
|
||||
Weather
|
||||
Saturday 9:00 PM
|
||||
Clear",
|
||||
)]),
|
||||
)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
);
|
||||
|
||||
let (response2, _) = self
|
||||
@@ -318,10 +323,15 @@ impl ProviderTester {
|
||||
);
|
||||
let tool_response = Message::user().with_tool_response(
|
||||
"test_id",
|
||||
Ok(vec![Content::image(
|
||||
image_content.data.clone(),
|
||||
image_content.mime_type.clone(),
|
||||
)]),
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::image(
|
||||
image_content.data.clone(),
|
||||
image_content.mime_type.clone(),
|
||||
)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
);
|
||||
|
||||
let result2 = self
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "How to Stop Your AI Agent From Making Unwanted Code Changes"
|
||||
description: Teach your AI agent how to commit early and often so you can control changes, roll back safely, and keep clean snapshots of your codebase.
|
||||
authors:
|
||||
- rizel
|
||||
---
|
||||
|
||||

|
||||
|
||||
AI agents are often described as brilliant, overeager interns. They're desperate to help, but sometimes that enthusiasm leads to changes you never asked for. This is by design: the large language models powering agents are trained to be helpful. But in code, unchecked helpfulness can create chaos. Even with clear instructions and a meticulous plan, you might hear, "Let me just change this too…" A modification that's either unnecessary or, worse, never surfaced for review.
|
||||
|
||||
Sure, you can scour `git diff` to find and revert issues. But in a multi-step process touching dozens of files, untangling one small, unwanted change becomes a manual nightmare. I've spent hours combing through 70 files to undo a single "helpful" adjustment. Asking the agent to revert is often futile, as conversational memory isn't a snapshot of your codebase.
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
This problem has a classic engineering solution. We commit early and often to create checkpoints, enabling easy rollbacks and clean collaboration. So, why don't we enforce the same discipline on our AI agents? Here’s the workflow I use with [goose](/) to ensure we're creating snapshots of the codebase:
|
||||
|
||||
|
||||
### 1. Set Up Version Control
|
||||
|
||||
I set up the [GitHub CLI](https://cli.github.com/) (`gh`). I've found Goose interacts with it flawlessly. The [GitHub MCP Server](/docs/mcp/github-mcp) is a good alternative.
|
||||
|
||||
### 2. Branch First
|
||||
|
||||
Always start on a new feature branch. Never let an agent commit directly to main.
|
||||
|
||||
### 3. Set Rules in a Context File
|
||||
|
||||
This is the key. I use a [`.goosehints`](/docs/guides/using-goosehints) or [`AGENTS.md`](/docs/guides/using-goosehints#custom-context-files) file with one critical instruction:
|
||||
|
||||
> "Every time you make a change, make a commit with a clear message."
|
||||
|
||||
This does two things: it automates checkpointing so I don't have to babysit the session, and it captures perfect snapshots in time, turning the git history into an undo stack for the entire collaboration.
|
||||
|
||||
### 4. Collaborate with Confidence
|
||||
|
||||
Now I can prompt goose to build, fix, or refactor. If it veers off course or makes a design choice I dislike, I can instantly review the git log or simply say:
|
||||
|
||||
> *"Revert to commit abc123."*
|
||||
|
||||
## The Result
|
||||
Then, when I'm happy with the final changes, I can push my code to remote. By integrating this basic software practice, I replace anxiety with awareness. goose gets to be brilliantly helpful, and I get to stay in control.
|
||||
|
||||
Try out this method with [goose](/) to help you build your next project. Your future self (and your git history) will thank you.
|
||||
|
||||
<head>
|
||||
<meta property="og:title" content="How to Stop Your AI Agent From Making Unwanted Changes" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="https://block.github.io/goose/blog/2025/12/10/stop-ai-agent-unwanted-changes" />
|
||||
<meta property="og:description" content="AI agents are often described as brilliant, overeager interns. Learn how to keep them in control with simple version control practices." />
|
||||
<meta property="og:image" content="https://block.github.io/goose/assets/images/header-image-ce224702149226ea0924fac736eef2fa.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="block.github.io/goose" />
|
||||
<meta name="twitter:title" content="How to Stop Your AI Agent From Making Unwanted Changes" />
|
||||
<meta name="twitter:description" content="AI agents are often described as brilliant, overeager interns. Learn how to keep them in control with simple version control practices." />
|
||||
<meta name="twitter:image" content="https://block.github.io/goose/assets/images/header-image-ce224702149226ea0924fac736eef2fa.png" />
|
||||
</head>
|
||||
@@ -320,7 +320,7 @@ You can change your LLM provider and/or model or update your API key at any time
|
||||
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar.
|
||||
2. Click the `Settings` button on the sidebar.
|
||||
3. Click the `Models` tab.
|
||||
4. Choose to update your provider, switch models, or click `Reset Provider and Model` to clear your settings and return to the welcome screen. See details about these [configuration options](/docs/getting-started/providers#configure-provider).
|
||||
4. Choose to update your provider, switch models, or click `Reset Provider and Model` to clear your settings and return to the welcome screen. See details about these [configuration options](/docs/getting-started/providers#configure-provider-and-model).
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
1. Run the following command:
|
||||
|
||||
@@ -42,7 +42,7 @@ goose is compatible with a wide range of LLM providers, allowing you to choose a
|
||||
| [Venice AI](https://venice.ai/home) | Provides access to open source models like Llama, Mistral, and Qwen while prioritizing user privacy. **Requires an account and an [API key](https://docs.venice.ai/overview/guides/generating-api-key)**. | `VENICE_API_KEY`, `VENICE_HOST` (optional), `VENICE_BASE_PATH` (optional), `VENICE_MODELS_PATH` (optional) |
|
||||
| [xAI](https://x.ai/) | Access to xAI's Grok models including grok-3, grok-3-mini, and grok-3-fast with 131,072 token context window. | `XAI_API_KEY`, `XAI_HOST` (optional) |
|
||||
|
||||
## CLI Providers
|
||||
### CLI Providers
|
||||
|
||||
goose also supports special "pass-through" providers that work with existing CLI tools, allowing you to use your subscriptions instead of paying per token:
|
||||
|
||||
@@ -56,10 +56,9 @@ goose also supports special "pass-through" providers that work with existing CLI
|
||||
CLI providers are cost-effective alternatives that use your existing subscriptions. They work differently from API providers as they execute CLI commands and integrate with the tools' native capabilities. See the [CLI Providers guide](/docs/guides/cli-providers) for detailed setup instructions.
|
||||
:::
|
||||
|
||||
|
||||
## Configure Provider
|
||||
## Configure Provider and Model
|
||||
|
||||
To configure your chosen provider or see available options, visit the `Models` tab in goose Desktop or run `goose configure` in the CLI.
|
||||
To configure your chosen provider, see available options, or select a model, visit the `Models` tab in goose Desktop or run `goose configure` in the CLI.
|
||||
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
@@ -89,7 +88,7 @@ To configure your chosen provider or see available options, visit the `Models` t
|
||||
|
||||
<TabItem value="others" label="Other Providers">
|
||||
1. If you have a specific provider you want to use with goose, and an API key from that provider, choose `Other Providers`.
|
||||
2. Find the provider of your choice and click its `Configure` button. If you don't see your provider in the list, click `Add Custom Provider` at the bottom of the window.
|
||||
2. Find the provider of your choice and click its `Configure` button. If you don't see your provider in the list, click `Add Custom Provider` at the bottom of the window to [configure a custom provider](#configure-custom-provider).
|
||||
3. Depending on your provider, you'll need to input your API Key, API Host, or other optional [parameters](#available-providers). Click the `Submit` button to authenticate and begin your first session.
|
||||
|
||||
:::info Ollama Model Detection
|
||||
@@ -128,84 +127,121 @@ To configure your chosen provider or see available options, visit the `Models` t
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
1. In your terminal, run the following command:
|
||||
|
||||
```sh
|
||||
goose configure
|
||||
```
|
||||
```sh
|
||||
goose configure
|
||||
```
|
||||
|
||||
2. Select `Configure Providers` from the menu and press Enter.
|
||||
2. Select `Configure Providers` from the menu and press `Enter`.
|
||||
|
||||
```
|
||||
┌ goose-configure
|
||||
│
|
||||
◆ What would you like to configure?
|
||||
│ ● Configure Providers (Change provider or update credentials)
|
||||
│ ○ Add Extension
|
||||
│ ○ Toggle Extensions
|
||||
│ ○ Remove Extension
|
||||
│ ○ goose Settings
|
||||
└
|
||||
```
|
||||
3. Choose a model provider and press Enter.
|
||||
```
|
||||
┌ goose-configure
|
||||
│
|
||||
◆ What would you like to configure?
|
||||
// highlight-start
|
||||
│ ● Configure Providers (Change provider or update credentials)
|
||||
// highlight-end
|
||||
│ ○ Custom Providers
|
||||
│ ○ Add Extension
|
||||
│ ○ Toggle Extensions
|
||||
│ ○ Remove Extension
|
||||
│ ○ goose Settings
|
||||
└
|
||||
```
|
||||
3. Choose a model provider and press `Enter`. Use the arrow keys (↑/↓) to move through the options.
|
||||
|
||||
```
|
||||
┌ goose-configure
|
||||
│
|
||||
◇ What would you like to configure?
|
||||
│ Configure Providers
|
||||
│
|
||||
◆ Which model provider should we use?
|
||||
│ ● Anthropic (Claude and other models from Anthropic)
|
||||
│ ○ Azure OpenAI
|
||||
│ ○ Amazon Bedrock
|
||||
│ ○ Claude Code
|
||||
│ ○ Databricks
|
||||
│ ○ ...
|
||||
└
|
||||
```
|
||||
4. Enter your API key (and any other configuration details) when prompted.
|
||||
```
|
||||
┌ goose-configure
|
||||
│
|
||||
◇ What would you like to configure?
|
||||
│ Configure Providers
|
||||
│
|
||||
◆ Which model provider should we use?
|
||||
│ ○ Amazon Bedrock
|
||||
│ ○ Amazon SageMaker TGI
|
||||
// highlight-start
|
||||
│ ● Anthropic (Claude and other models from Anthropic)
|
||||
// highlight-end
|
||||
│ ○ Azure OpenAI
|
||||
│ ○ Claude Code CLI
|
||||
│ ○ ...
|
||||
└
|
||||
```
|
||||
4. Enter your API key (and any other configuration details) when prompted.
|
||||
|
||||
```
|
||||
┌ goose-configure
|
||||
│
|
||||
◇ What would you like to configure?
|
||||
│ Configure Providers
|
||||
│
|
||||
◇ Which model provider should we use?
|
||||
│ Anthropic
|
||||
│
|
||||
◆ Provider Anthropic requires ANTHROPIC_API_KEY, please enter a value
|
||||
│ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
|
||||
└
|
||||
```
|
||||
5. Enter your desired `ANTHROPIC_HOST` or you can use the default one by hitting the `Enter` key.
|
||||
```
|
||||
┌ goose-configure
|
||||
│
|
||||
◇ What would you like to configure?
|
||||
│ Configure Providers
|
||||
│
|
||||
◇ Which model provider should we use?
|
||||
│ Anthropic
|
||||
│
|
||||
◆ Provider Anthropic requires ANTHROPIC_API_KEY, please enter a value
|
||||
// highlight-start
|
||||
│ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
|
||||
// highlight-end
|
||||
└
|
||||
```
|
||||
|
||||
If you're just changing models, skip any prompts to update the provider configuration.
|
||||
|
||||
```
|
||||
◇ Enter new value for ANTHROPIC_HOST
|
||||
│ https://api.anthropic.com (default)
|
||||
```
|
||||
6. Enter the model you want to use or you can use the default one by hitting the `Enter` key.
|
||||
```
|
||||
│
|
||||
◇ Model fetch complete
|
||||
│
|
||||
◇ Enter a model from that provider:
|
||||
│ claude-sonnet-4-0 (default)
|
||||
│
|
||||
◓ Checking your configuration...
|
||||
└ Configuration saved successfully
|
||||
5. Enter your desired `ANTHROPIC_HOST` or press `Enter` to use the default.
|
||||
|
||||
```
|
||||
◆ Provider Anthropic requires ANTHROPIC_HOST, please enter a value
|
||||
// highlight-start
|
||||
│ https://api.anthropic.com (default)
|
||||
// highlight-end
|
||||
```
|
||||
6. Choose the model you want to use. Depending on the provider, you can:
|
||||
- Select the model from a list
|
||||
- Search for the model by name
|
||||
- Enter the model name directly
|
||||
|
||||
```
|
||||
│
|
||||
◇ Model fetch complete
|
||||
│
|
||||
◇ Select a model:
|
||||
// highlight-start
|
||||
│ claude-sonnet-4-5 (default)
|
||||
// highlight-end
|
||||
│
|
||||
◒ Checking your configuration...
|
||||
└ Configuration saved successfully
|
||||
```
|
||||
|
||||
This change takes effect the next time you start a session.
|
||||
|
||||
:::note
|
||||
`goose configure` doesn't support entering custom model names. To use a model not in the provider's list, use goose Desktop or edit the `GOOSE_MODEL` variable in your [`config.yaml`](/docs/guides/config-files) directly.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Set the model for an individual session using the [`run` command](/docs/guides/goose-cli-commands#run-options):
|
||||
|
||||
```bash
|
||||
goose run --model claude-sonnet-4-0 -t "initial prompt"
|
||||
```
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using Custom OpenAI Endpoints
|
||||
### Using Custom OpenAI Endpoints
|
||||
|
||||
goose supports using custom OpenAI-compatible endpoints, which is particularly useful for:
|
||||
The built-in OpenAI provider can connect to OpenAI's official API (`api.openai.com`) or any OpenAI-compatible endpoint, such as:
|
||||
- Self-hosted LLMs (e.g., LLaMA, Mistral) using vLLM or KServe
|
||||
- Private OpenAI-compatible API servers
|
||||
- Enterprise deployments requiring data governance and security compliance
|
||||
- OpenAI API proxies or gateways
|
||||
|
||||
### Configuration Parameters
|
||||
:::tip Custom Provider Option
|
||||
Need to connect to multiple OpenAI-compatible endpoints? [Configure custom providers](#configure-custom-provider) instead for easier switching and better organization, as well as custom naming and shareable configurations.
|
||||
:::
|
||||
|
||||
#### Configuration Parameters
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
@@ -215,7 +251,7 @@ goose supports using custom OpenAI-compatible endpoints, which is particularly u
|
||||
| `OPENAI_PROJECT` | No | Project identifier for resource management |
|
||||
| `OPENAI_CUSTOM_HEADERS` | No | Additional headers to include in the request. Can be set via environment variable, configuration file, or CLI, in the format `HEADER_A=VALUE_A,HEADER_B=VALUE_B`. |
|
||||
|
||||
### Example Configurations
|
||||
#### Example Configurations
|
||||
|
||||
<Tabs groupId="deployment">
|
||||
<TabItem value="vllm" label="vLLM Self-Hosted" default>
|
||||
@@ -253,7 +289,7 @@ goose supports using custom OpenAI-compatible endpoints, which is particularly u
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Setup Instructions
|
||||
#### Setup Instructions
|
||||
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
@@ -285,6 +321,286 @@ goose supports using custom OpenAI-compatible endpoints, which is particularly u
|
||||
For enterprise deployments, you can pre-configure these values using environment variables or configuration files to ensure consistent governance across your organization.
|
||||
:::
|
||||
|
||||
## Configure Custom Provider
|
||||
|
||||
Custom providers let you connect to services that aren't in the [available providers](#available-providers) list. They appear in goose's provider list and can be selected like any other provider.
|
||||
|
||||
**Benefits:**
|
||||
- **Multiple endpoints**: Switch between different services (e.g., vLLM, corporate proxy, OpenAI)
|
||||
- **Pre-configured models**: Store a list of preferred models
|
||||
- **Shareable configuration**: JSON files can be shared across teams or checked into repos
|
||||
- **Custom naming**: Show "Corporate API" instead of "OpenAI" in the UI
|
||||
- **Separate credentials**: Assign each provider its own API key
|
||||
|
||||
Custom providers must use OpenAI, Anthropic, or Ollama compatible API formats. OpenAI-compatible providers can include custom headers for additional authentication, API keys, tokens, or tenant identifiers. Each custom provider maps to a JSON configuration file.
|
||||
|
||||
**To add a custom provider:**
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
|
||||
2. Click the `Settings` button on the sidebar
|
||||
3. Click the `Models` tab
|
||||
4. Click `Configure providers`
|
||||
5. Click `Add Custom Provider` at the bottom of the window
|
||||
6. Fill in the provider details:
|
||||
- **Provider Type**:
|
||||
- `OpenAI Compatible` (most common)
|
||||
- `Anthropic Compatible`
|
||||
- `Ollama Compatible`
|
||||
- **Display Name**: A friendly name for the provider
|
||||
- **API URL**: The base URL of the API endpoint
|
||||
- **API Key**: The API key, which is accessed using a custom environment variable and stored in the keychain (or `secrets.yaml` if the keyring is disabled)
|
||||
- For `Ollama Compatible` providers, click `This is a local model (no auth required)`
|
||||
- **Available Models**: Comma-separated list of available model names
|
||||
- **Streaming Support**: Whether the API supports streaming responses (click to toggle)
|
||||
7. Click `Create Provider`
|
||||
|
||||
:::info Custom Headers
|
||||
Currently, custom headers for OpenAI compatible providers can't be defined in goose Desktop. As a workaround, configure the provider using goose CLI or edit the provider configuration file directly.
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
1. In your terminal, run the following command:
|
||||
|
||||
```sh
|
||||
goose configure
|
||||
```
|
||||
|
||||
2. Select `Custom Providers`. Use the arrow keys (↑/↓) to move through the options.
|
||||
|
||||
```sh
|
||||
┌ goose-configure
|
||||
│
|
||||
◆ What would you like to configure?
|
||||
│ ○ Configure Providers
|
||||
// highlight-start
|
||||
│ ● Custom Providers (Add custom provider with compatible API)
|
||||
// highlight-end
|
||||
│ ○ Add Extension
|
||||
│ ○ Toggle Extensions
|
||||
│ ○ Remove Extension
|
||||
│ ○ goose Settings
|
||||
└
|
||||
```
|
||||
|
||||
3. Select `Add A Custom Provider`
|
||||
|
||||
```sh
|
||||
┌ goose-configure
|
||||
│
|
||||
◇ What would you like to configure?
|
||||
│ Custom Providers
|
||||
│
|
||||
◆ What would you like to do?
|
||||
// highlight-start
|
||||
│ ● Add A Custom Provider (Add a new OpenAI/Anthropic/Ollama compatible Provider)
|
||||
// highlight-end
|
||||
│ ○ Remove Custom Provider
|
||||
└
|
||||
```
|
||||
|
||||
4. Follow the prompts to enter the provider details:
|
||||
- **API Type**:
|
||||
- `OpenAI Compatible` (most common)
|
||||
- `Anthropic Compatible`
|
||||
- `Ollama Compatible`
|
||||
- **Name**: A friendly name for the provider
|
||||
- **API URL**: The base URL of the API endpoint
|
||||
- **API Key**: The API key, which is accessed using a custom environment variable and stored in the keychain (or `secrets.yaml` if the keyring is disabled)
|
||||
- For `Ollama Compatible` providers, press `Enter` to skip (or enter any value to be able to use the provider in goose Desktop)
|
||||
- **Available Models**: Comma-separated list of available model names
|
||||
- **Streaming Support**: Whether the API supports streaming responses
|
||||
- **Custom Headers**: Required header names and values (`OpenAI Compatible` providers only)
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="config" label="Config File">
|
||||
|
||||
First create a JSON file in the `custom_providers` directory:
|
||||
- macOS/Linux: `~/.config/goose/custom_providers/`
|
||||
- Windows: `%APPDATA%\Block\goose\config\custom_providers\`
|
||||
|
||||
Example `custom_corp_api.json` configuration file:
|
||||
```json
|
||||
{
|
||||
"name": "custom_corp_api",
|
||||
"engine": "openai",
|
||||
"display_name": "Corporate API",
|
||||
"description": "Custom Corporate API provider",
|
||||
"api_key_env": "CUSTOM_CORP_API_API_KEY",
|
||||
"base_url": "https://api.company.com/v1/chat/completions",
|
||||
"models": [
|
||||
{
|
||||
"name": "gpt-4o",
|
||||
"context_limit": 128000
|
||||
},
|
||||
{
|
||||
"name": "gpt-3.5-turbo",
|
||||
"context_limit": 16385
|
||||
}
|
||||
],
|
||||
"headers": {
|
||||
"x-origin-client-id": "YOUR_CLIENT_ID",
|
||||
"x-origin-secret": "YOUR_SECRET_VALUE"
|
||||
},
|
||||
"supports_streaming": true
|
||||
}
|
||||
```
|
||||
|
||||
Then use the `api_key_env` to set the key for your session. For example:
|
||||
```bash
|
||||
export CUSTOM_CORP_API_API_KEY="your-api-key"
|
||||
goose session start --provider custom_corp_api
|
||||
```
|
||||
|
||||
:::tip Keychain Key Storage
|
||||
If you want to store the API key in the `goose` keychain, update the provider in goose Desktop and enter the key. This provides secure, persistent storage and allows goose to connect natively to the provider.
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
**To update a custom provider:**
|
||||
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
|
||||
2. Click the `Settings` button on the sidebar
|
||||
3. Click the `Models` tab
|
||||
4. Click `Configure providers`
|
||||
5. Click on your custom provider in the list
|
||||
6. Update the fields you want to change
|
||||
<br/>**Important:** Verify that `Provider Type` shows the correct value before saving. Otherwise, it may default to `OpenAI Compatible` regardless of the original setting.
|
||||
7. Click `Update Provider`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
|
||||
1. In your terminal, run the following command:
|
||||
|
||||
```sh
|
||||
goose configure
|
||||
```
|
||||
|
||||
2. Select `Configure Providers` from the menu and press `Enter`.
|
||||
|
||||
```sh
|
||||
┌ goose-configure
|
||||
│
|
||||
◆ What would you like to configure?
|
||||
// highlight-start
|
||||
│ ● Configure Providers (Change provider or update credentials)
|
||||
// highlight-end
|
||||
│ ○ Custom Providers
|
||||
│ ○ Add Extension
|
||||
│ ○ Toggle Extensions
|
||||
│ ○ Remove Extension
|
||||
│ ○ goose Settings
|
||||
└
|
||||
```
|
||||
|
||||
3. Select the custom provider you want to update and press `Enter`. Use the arrow keys (↑/↓) to move through the options.
|
||||
|
||||
```sh
|
||||
┌ goose-configure
|
||||
│
|
||||
◇ What would you like to configure?
|
||||
│ Configure Providers
|
||||
│
|
||||
◆ Which model provider should we use?
|
||||
│ ○ Amazon Bedrock
|
||||
│ ○ Amazon SageMaker TGI
|
||||
│ ○ Anthropic
|
||||
│ ○ Azure OpenAI
|
||||
│ ○ Claude Code CLI
|
||||
// highlight-start
|
||||
│ ● Corporate API (Custom Corporate API provider)
|
||||
// highlight-end
|
||||
│ ○ Cursor Agent
|
||||
│ ○ ...
|
||||
└
|
||||
```
|
||||
|
||||
4. Follow the prompts to update the fields.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="config" label="Config File">
|
||||
|
||||
Open the custom provider configuration file in the `custom_providers` directory:
|
||||
- macOS/Linux: `~/.config/goose/custom_providers/`
|
||||
- Windows: `%APPDATA%\Block\goose\config\custom_providers\`
|
||||
|
||||
Update the fields you want to change and save your changes.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Your changes are available in your next goose session.
|
||||
|
||||
**To remove a custom provider:**
|
||||
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
Currently you cannot remove custom providers using goose Desktop.
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
|
||||
1. In your terminal, run the following command:
|
||||
|
||||
```sh
|
||||
goose configure
|
||||
```
|
||||
|
||||
2. Select `Custom Providers`. Use the arrow keys (↑/↓) to move through the options.
|
||||
|
||||
```sh
|
||||
┌ goose-configure
|
||||
│
|
||||
◆ What would you like to configure?
|
||||
│ ○ Configure Providers
|
||||
// highlight-start
|
||||
│ ● Custom Providers (Add custom provider with compatible API)
|
||||
// highlight-end
|
||||
│ ○ Add Extension
|
||||
│ ○ Toggle Extensions
|
||||
│ ○ Remove Extension
|
||||
│ ○ goose Settings
|
||||
└
|
||||
```
|
||||
|
||||
3. Select `Remove Custom Provider`.
|
||||
|
||||
```sh
|
||||
┌ goose-configure
|
||||
│
|
||||
◇ What would you like to configure?
|
||||
│ Custom Providers
|
||||
│
|
||||
◆ What would you like to do?
|
||||
│ ○ Add A Custom Provider
|
||||
// highlight-start
|
||||
│ ● Remove Custom Provider (Remove an existing custom provider)
|
||||
// highlight-end
|
||||
└
|
||||
```
|
||||
|
||||
4. Select the custom provider you want to remove.
|
||||
|
||||
The provider configuration file is removed from the `custom_providers` directory and the key is removed from the keychain.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="config" label="Config File">
|
||||
|
||||
:::tip
|
||||
If the provider's API key is stored in the keychain, use goose CLI to remove the custom provider. This also removes the stored API key.
|
||||
:::
|
||||
|
||||
Delete the custom provider configuration file in the `custom_providers` directory:
|
||||
- macOS/Linux: `~/.config/goose/custom_providers/`
|
||||
- Windows: `%APPDATA%\Block\goose\config\custom_providers\`
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using goose for Free
|
||||
|
||||
goose is a free and open source AI agent that you can start using right away, but not all supported [LLM Providers][providers] provide a free tier.
|
||||
|
||||
@@ -45,8 +45,8 @@ The following settings can be configured at the root level of your config.yaml f
|
||||
| `GOOSE_AUTO_COMPACT_THRESHOLD` | Set the percentage threshold at which goose [automatically summarizes your session](/docs/guides/sessions/smart-context-management#automatic-compaction). | Float between 0.0 and 1.0 (disabled at 0.0)| 0.8 | No |
|
||||
| `otel_exporter_otlp_endpoint` | OTLP endpoint URL for [observability](/docs/guides/environment-variables#opentelemetry-protocol-otlp) | URL (e.g., `http://localhost:4318`) | None | No |
|
||||
| `otel_exporter_otlp_timeout` | Export timeout in milliseconds for [observability](/docs/guides/environment-variables#opentelemetry-protocol-otlp) | Integer (ms) | 10000 | No |
|
||||
| `security_prompt_enabled` | Enable [prompt injection detection](/docs/guides/security/prompt-injection-detection) to identify potentially harmful commands | true/false | false | No |
|
||||
| `security_prompt_threshold` | Sensitivity threshold for [prompt injection detection](/docs/guides/security/prompt-injection-detection) (higher = stricter) | Float between 0.01 and 1.0 | 0.7 | No |
|
||||
| `SECURITY_PROMPT_ENABLED` | Enable [prompt injection detection](/docs/guides/security/prompt-injection-detection) to identify potentially harmful commands | true/false | false | No |
|
||||
| `SECURITY_PROMPT_THRESHOLD` | Sensitivity threshold for [prompt injection detection](/docs/guides/security/prompt-injection-detection) (higher = stricter) | Float between 0.01 and 1.0 | 0.7 | No |
|
||||
|
||||
## Experimental Features
|
||||
|
||||
@@ -91,7 +91,7 @@ otel_exporter_otlp_endpoint: "http://localhost:4318"
|
||||
otel_exporter_otlp_timeout: 20000
|
||||
|
||||
# Security Configuration
|
||||
security_prompt_enabled: true
|
||||
SECURITY_PROMPT_ENABLED: true
|
||||
|
||||
# Extensions Configuration
|
||||
extensions:
|
||||
|
||||
@@ -16,8 +16,8 @@ These are the minimum required variables to get started with goose.
|
||||
|
||||
| Variable | Purpose | Values | Default |
|
||||
|----------|---------|---------|---------|
|
||||
| `GOOSE_PROVIDER` | Specifies the LLM provider to use | [See available providers](/docs/getting-started/providers#available-providers) | None (must be [configured](/docs/getting-started/providers#configure-provider)) |
|
||||
| `GOOSE_MODEL` | Specifies which model to use from the provider | Model name (e.g., "gpt-4", "claude-sonnet-4-20250514") | None (must be configured) |
|
||||
| `GOOSE_PROVIDER` | Specifies the LLM provider to use | [See available providers](/docs/getting-started/providers#available-providers) | None (must be [configured](/docs/getting-started/providers#configure-provider-and-model)) |
|
||||
| `GOOSE_MODEL` | Specifies which model to use from the provider | Model name (e.g., "gpt-4", "claude-sonnet-4-20250514") | None (must be [configured](/docs/getting-started/providers#configure-provider-and-model)) |
|
||||
| `GOOSE_TEMPERATURE` | Sets the [temperature](https://medium.com/@kelseyywang/a-comprehensive-guide-to-llm-temperature-%EF%B8%8F-363a40bbc91f) for model responses | Float between 0.0 and 1.0 | Model-specific default |
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -536,6 +536,21 @@ While the web interface provides most core features, be aware of these limitatio
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Terminal Integration
|
||||
|
||||
#### @goose / @g
|
||||
Ask goose questions directly from your shell prompt, with command history included in the context. These aliases are created when you set up [terminal integration](/docs/guides/terminal-integration.md).
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Ask questions with command history context
|
||||
@goose create a python script to process these files
|
||||
@goose create a PR description summarizing these changes
|
||||
@g how do I fix these permission denied errors?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactive Session Features
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PanelLeft } from 'lucide-react';
|
||||
|
||||
Rate limiting is the process of restricting the number of requests a user or application can send to an LLM API within a specific timeframe. LLM providers enforce this with the purpose of managing resources and preventing abuse.
|
||||
|
||||
Since goose is working very quickly to implement your tasks, you may need to manage rate limits imposed by the provider. If you frequently hit rate limits, consider upgrading your LLM plan to access higher tier limits or [configure a provider](/docs/getting-started/providers#configure-provider) that has built-in rate limiting:
|
||||
Since goose is working very quickly to implement your tasks, you may need to manage rate limits imposed by the provider. If you frequently hit rate limits, consider upgrading your LLM plan to access higher tier limits or [configure a provider](/docs/getting-started/providers#configure-provider-and-model) that has built-in rate limiting:
|
||||
|
||||
:::info
|
||||
goose supports automatic setup for both providers that takes you through OAuth account creation and secure API key generation.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
sidebar_position: 55
|
||||
title: MCP Elicitation
|
||||
sidebar_label: MCP Elicitation
|
||||
description: How extensions can request structured information from you during a task
|
||||
unlisted: true
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
MCP Elicitation allows goose to pause and ask you for specific information when an extension needs it. Instead of guessing or making assumptions, goose presents a form requesting exactly what's needed to continue.
|
||||
|
||||
This feature is automatically enabled in goose. When an extension that supports elicitation needs information from you, a form will appear in your session.
|
||||
|
||||
:::info
|
||||
[MCP Elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation) is a feature in the Model Context Protocol. goose supports form mode requests.
|
||||
:::
|
||||
|
||||
## How MCP Elicitation Works
|
||||
|
||||
When an extension needs information, goose pauses and presents a form for you to fill out. You can submit your response or cancel the request.
|
||||
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
|
||||
A form appears inline in the chat with:
|
||||
- Fields for the requested data
|
||||
- Required fields marked with an asterisk (*)
|
||||
- Default values you can accept or change
|
||||
- A **Submit** button to send your response
|
||||
|
||||
After submitting, you'll see a confirmation message.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
|
||||
A prompt appears in your terminal with:
|
||||
- A message explaining what information is needed (in cyan)
|
||||
- Field names (in yellow) with descriptions
|
||||
- Required fields marked with a red asterisk (*)
|
||||
- Default values shown in brackets, e.g., `[default]`
|
||||
|
||||
Type your response for each field and press Enter. For yes/no questions, you'll see an interactive toggle.
|
||||
|
||||
To cancel the request, press `Ctrl+C`.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info Timeout
|
||||
Elicitation requests timeout after 5 minutes. If you don't respond in time, the request is cancelled and goose will continue without the information.
|
||||
:::
|
||||
|
||||
## For Extension Developers
|
||||
|
||||
Want to add elicitation to your own extensions? See the [MCP Elicitation specification](https://modelcontextprotocol.io/specification/draft/client/elicitation) to learn how MCP servers can request structured input from users.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user