From 176a81412ae5c7aceabf4ac48133c37b49aa505b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 28 Aug 2026 15:15:10 -0600 Subject: [PATCH] feat: improved first-time run experience and included a templated configuration file that now has comments like the config.example.yaml so users don't have to go to the repo to see all the knobs --- assets/config-template.yaml | 279 +++++++++++++++++++++++++ config.example.yaml | 380 ++++++++++++++++------------------ src/config/app_state.rs | 2 +- src/config/mod.rs | 174 ++++++++-------- src/config/request_context.rs | 4 +- src/function/mod.rs | 166 ++++++++++++--- 6 files changed, 688 insertions(+), 317 deletions(-) create mode 100644 assets/config-template.yaml diff --git a/assets/config-template.yaml b/assets/config-template.yaml new file mode 100644 index 0000000..02ca30f --- /dev/null +++ b/assets/config-template.yaml @@ -0,0 +1,279 @@ +# Coyote configuration. Generated by the first-run wizard. +# Every setting is listed with its effective value and a short description. +# For richer examples of each section, see +# https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml + +# ---- LLM ---- +__MODEL_BLOCK__ +temperature: null # Set default temperature parameter (0, 1) +top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2) depending on the model + +# ---- Behavior ---- +dry_run: false # Display the messages that would be sent to the LLM without actually sending them +stream: true # Controls whether to use the stream-style APIs when querying for completions from LLM clients +save: true # Indicates whether to persist the conversation to messages.md for posterity +keybindings: emacs # Choose keybinding style (emacs, vi) +editor: null # Specifies the editor used to edit the input buffer or session. (e.g. vim, emacs, nano, hx). Defaults to $EDITOR +wrap: auto # Controls text wrapping (no, auto, ) +wrap_code: false # Enables or disables the wrapping of code blocks + +# ---- Vault ---- +# See the [Vault documentation](https://github.com/Dark-Alex-17/coyote/wiki/Vault) for more information on the Coyote vault. +# +# The secrets_provider tells Coyote where to read and write secrets referenced via {{SECRET_NAME}} syntax. +# +# Shorthand: set vault_password_file to enable the local provider with that password +# file (it cannot be a secret template). +# +# Explicit: set secrets_provider to one of the supported types below. When secrets_provider is set, +# vault_password_file is ignored. Note: secrets_provider itself cannot use secret template syntax. +# The vault must be initialized before any secrets can be resolved. +# +# Local (same as the shorthand above): +# secrets_provider: +# type: local +# password_file: ~/.coyote_password +# +# AWS Secrets Manager (requires an authenticated AWS CLI; see `aws sso login` or `aws configure`): +# secrets_provider: +# type: aws_secrets_manager +# aws_profile: default +# aws_region: us-east-1 +# +# GCP Secret Manager (requires `gcloud auth application-default login`): +# secrets_provider: +# type: gcp_secret_manager +# gcp_project_id: my-project-id +# +# Azure Key Vault (requires `az login`): +# secrets_provider: +# type: azure_key_vault +# vault_name: my-vault-name +# +# gopass (requires the `gopass` CLI to be installed and initialized): +# secrets_provider: +# type: gopass +# store: my-store # Optional; omit to use the default store +# +# 1Password (requires the `op` CLI to be installed and signed in via `op signin`): +# secrets_provider: +# type: one_password +# vault: Production # Optional; omit to use the default vault +# account: my.1password.com # Optional; omit to use the default account +__SECRETS_BLOCK__ + +# ---- Function Calling ---- +# See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details +function_calling_support: true # Enables or disables function calling (globally) +mapping_tools: {} # Alias for a tool or toolset + # Example: + # mapping_tools: + # fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep' +enabled_tools: null # Which tools to enable by default. + # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. + # Example (list form): + # enabled_tools: + # - fs + # - web_search_coyote + # Example (comma-separated form): + # enabled_tools: fs,web_search_coyote +visible_tools: null # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools'). + # Null/missing = all tools in the global tools dir are visible; [] = none; + # an explicit list makes only those tools visible. + # Example: + # visible_tools: + # - execute_command.sh + # - fs_cat.sh + # - fs_ls.sh + +# ---- Skills ---- +# Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation. +# See the [Skills documentation](https://github.com/Dark-Alex-17/coyote/wiki/Skills) for more details. +skills_enabled: true # Master switch. Set to false to hide all skill management tools from the model. + # Skills also require `function_calling_support: true` above to work at all. +enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible. + # Accepts either a YAML list or a comma-separated string. + # Example (list form): + # enabled_skills: + # - git-master + # - ai-slop-remover + # Example (comma-separated form): + # enabled_skills: git-master,ai-slop-remover +visible_skills: null # The universe of skills allowed to be enabled in any context. null = all installed. + # Example: + # visible_skills: + # - ai-slop-remover + # - code-review + # - git-master + +# ---- Macros ---- +# Macros are Coyote's custom commands: named sequences of REPL commands and prompts, invoked directly by name +# (a macro file named `review.yaml` runs as `.review [args]`; built-in commands always win a name collision). +# Workspace-local macros in `.coyote/macros/` shadow same-named global macros (skip them with --no-workspace-macros). +# See the [Macros documentation](https://github.com/Dark-Alex-17/coyote/wiki/Macros) for more details. +enabled_macros: null # Which macros are invocable by default (no role/agent/session active). null = all visible. + # An empty list means NO macros are invocable. Accepts either a YAML list or a + # comma-separated string. Roles, agents, and sessions may define their own + # `enabled_macros`; the most specific active one wins (session > agent > role > global). + # Example (list form): + # enabled_macros: + # - generate-commit-message + # Example (comma-separated form): + # enabled_macros: generate-commit-message,review + +# ---- MCP Servers ---- +# See the [MCP Servers documentation](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) for more details +mcp_server_support: true # Enables or disables MCP servers (globally) +mapping_mcp_servers: {} # Alias for an MCP server or set of servers + # Example: + # mapping_mcp_servers: + # git: github,gitmcp +enabled_mcp_servers: null # Which MCP servers to enable by default. + # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. + # Example (list form): + # enabled_mcp_servers: + # - github + # - slack + # Example (comma-separated form): + # enabled_mcp_servers: github,slack,ddg-search +mcp_tools: null # Per-server MCP tool allowlists (glob patterns: * and ? supported). + # Tools that match no pattern are hidden from the model as if they + # don't exist. Stacks with the other allowlist layers (mcp.json + # `allowedTools`, role, agent, session, skill, graph node). Every + # configured layer must allow a tool, so layers only ever narrow. + # An empty list blocks all of a server's tools. + # Example: + # mcp_tools: + # github: + # - get_* + # - list_* + # slack: [] + +# ---- Auto-Continue (Todo System) ---- +# The auto-continue system provides built-in task tracking for improved reliability. +# When enabled, the model can create todo lists and the system will automatically +# prompt it to continue when incomplete tasks remain. +# See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information +auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false) +max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10) +inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true) +continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default +inject_skill_instructions: true # Inject a short hint pointing the model at `skill__list` when skills are enabled + # in this context. Only injected if `function_calling_support`, `skills_enabled`, and the + # effective enabled skill set is non-empty (default: true) +skill_instructions: null # Custom text used for the skill hint when injected. If null, uses built-in default + +# ---- Prelude ---- +repl_prelude: null # Set a default session or role for REPL mode to use (e.g. role:, session:, :) +cmd_prelude: null # Set a default session or role for CMD mode to use (e.g. role:, session:, :) +agent_session: null # Set a session to use when starting an agent (e.g. temp, default) + +# ---- Session ---- +# See the [Session documentation](https://github.com/Dark-Alex-17/coyote/wiki/Sessions) for more information +save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save; if null, ask the user what to do +compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold +compression_keep_last: 0 # Number of most-recent messages to keep visible after compression (0 = compress all messages) +summarization_prompt: null # The text prompt used for creating a concise summary of session messages. If null, uses built-in default +summary_context_prompt: null # The text prompt used for including the summary of the entire session as context to the model. If null, uses built-in default +max_tool_result_chars: null # Cap on tool result characters forwarded to the model per call (null = no cap) +max_concurrent_jobs: null # Max background jobs (`job__*` tools) running at once per context (null = 5; 0 disables background jobs entirely) + +# ---- Memory ---- +# See the [Memory documentation](https://github.com/Dark-Alex-17/coyote/wiki/Memory) for more information. +# Memory is opt-in by workspace presence (`.coyote/memory/MEMORY.md`) and global +# presence (`/memory/MEMORY.md`). Set `memory: false` to disable +# even when memory files exist. The cascade is: agent > session > role > app. +# Bootstrap with `coyote --init-memory [global|workspace]` to create the marker file +# the LLM needs before it will write any memory. +memory: null # null = enabled when memory exists on disk; true = force on; false = force off +memory_cap_with_tools: null # Char cap for injected memory when function calling is available (null = 6000). + # Only MEMORY.md indexes are injected; the LLM uses memory__read to fetch drill files. +memory_cap_without_tools: null # Char cap when function calling is unavailable (null = 12000). + # Indexes plus drill file bodies are injected up to this cap. + +# ---- Workspace Instructions ---- +# Human-curated project instructions injected read-only into the system prompt, in full. +# Coyote walks up from the current directory and injects the first match from the file +# chain below (per directory, in order). Scaffold with `coyote --init-instructions`. +# Disable per-invocation with --no-workspace-instructions, or override the chain with +# repeatable --workspace-instructions-file flags. +workspace_instructions: null # null/true = inject when an instructions file exists; false = never inject +workspace_instructions_files: null # File name chain to search, in priority order. + # Default: [COYOTE.md, AGENTS.md, CLAUDE.md, GEMINI.md] + # Set to a custom list to reorder or drop fallbacks, e.g.: + # workspace_instructions_files: [COYOTE.md] + +# ---- RAG ---- +# See the [RAG Docs](https://github.com/Dark-Alex-17/coyote/wiki/RAG) for more details. +rag_embedding_model: null # Specifies the embedding model used for context retrieval +rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Coyote uses Reciprocal Rank Fusion by default +rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries +rag_chunk_size: null # Defines the size of chunks for document processing in characters +rag_chunk_overlap: null # Defines the overlap between chunks +rag_template: null # Defines the query structure using variables like __CONTEXT__, __SOURCES__, and __INPUT__ + # to tailor searches to specific needs. If null, uses built-in default +rag_extractor_model: null # LLM model for graph-based entity/relationship extraction; when set, enables a graph RAG signal alongside vector and BM25 +rag_extractor_prompt: null # Custom extraction prompt template; must contain __CHUNK__ placeholder; defaults to built-in prompt when null +rag_graph_hops: 1 # Number of hops to expand from matched entities at query time (0 = seed nodes only; 1 = direct neighbors; increase for denser graphs) + +# Define document loaders to control how RAG and `.file`/`--file` load files of specific formats. +document_loaders: {} + # You can add custom loaders using the following syntax: + # : + # Note: Use `$1` for input file and `$2` for output file. If `$2` is omitted, use stdout as output. + # Examples: + # document_loaders: + # pdf: 'pdftotext $1 -' # https://poppler.freedesktop.org + # docx: 'pandoc --to plain $1' # https://pandoc.org + # jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}"' # Requires a Jina API key in the Coyote vault + +# ---- Appearance ---- +highlight: true # Controls syntax highlighting +raw_markdown: false # When true, render markdown as raw text with syntax highlighting only. When false (default), transforms markdown syntax (headings, bold, lists, etc.) into styled terminal output +theme: null # null = the built-in dark theme; set to `light` for the built-in light theme. + # Custom themes: place a `dark.tmTheme` or `light.tmTheme` file in the Coyote config + # directory and it is used in place of the corresponding built-in. + +# ---- REPL Prompt ---- +# Custom REPL left/right prompts; see the [REPL Prompt Documentation](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt) for more information +left_prompt: null # If null, uses the built-in default: + # '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} ' +right_prompt: null # If null, uses the built-in default: + # '{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}' + +# ---- Miscellaneous ---- +user_agent: null # Set User-Agent HTTP header, use `auto` for coyote/ +save_shell_history: true # Whether to save shell execution command to the history file +sync_models_url: null # URL to sync model changes from. If null, uses the built-in default: + # https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml + +# ---- Clients ---- +# See the [Clients documentation](https://github.com/Dark-Alex-17/coyote/wiki/Clients) for more details +# +# All clients have the following configuration: +# - type: xxxx +# name: xxxx # Only use it to distinguish clients with the same client type. Optional +# models: +# - name: xxxx # Chat model +# max_input_tokens: 100000 +# supports_vision: true +# supports_function_calling: true +# - name: xxxx # Embedding model +# type: embedding +# default_chunk_size: 1500 +# max_batch_size: 100 +# - name: xxxx # Reranker model +# type: reranker +# patch: # Patch API calls +# chat_completions: # API type; Possible values: chat_completions, embeddings, and rerank +# : # The regex to match model names, e.g. '.*' 'gpt-4o' 'gpt-4o|gpt-4-.*' +# url: '' # Patch request URL +# body: # Patch request body +# +# headers: # Patch request headers +# : +# extra: +# proxy: socks5://127.0.0.1:1080 # Set proxy +# connect_timeout: 10 # Set timeout in seconds for connect to api +# read_timeout: 300 # Set timeout in seconds for a read stall (no bytes received); 0 disables (default: 300) +__CLIENTS_BLOCK__ diff --git a/config.example.yaml b/config.example.yaml index 9425cbb..2b2a8eb 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,39 +1,40 @@ # ---- LLM ---- -model: openai:gpt-4o # Specify the LLM to use -temperature: null # Set default temperature parameter (0, 1) -top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2) depending on the model -reasoning_effort: - null # Reasoning effort level for models that support it (e.g. low, medium, high). - # Only valid when the active model declares reasoning_levels. See the Clients docs. +model: openai:gpt-4o # Specify the LLM to use +temperature: null # Set default temperature parameter (0, 1) +top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2) depending on the model +reasoning_effort: null # Reasoning effort level for models that support it (e.g. low, medium, high). + # Only valid when the active model declares reasoning_levels. See the Clients docs. # ---- Behavior ---- -stream: true # Controls whether to use the stream-style APIs when querying for completions from LLM clients. -save: true # Indicates whether to persist the conversation to messages.md for posterity -keybindings: emacs # Choose keybinding style (emacs, vi) -editor: null # Specifies the editor used to edit the input buffer or session. (e.g. vim, emacs, nano, hx). Defaults to $EDITOR -wrap: no # Controls text wrapping (no, auto, ) -wrap_code: false # Enables or disables the wrapping of code blocks +stream: true # Controls whether to use the stream-style APIs when querying for completions from LLM clients. +save: true # Indicates whether to persist the conversation to messages.md for posterity +keybindings: emacs # Choose keybinding style (emacs, vi) +editor: null # Specifies the editor used to edit the input buffer or session. (e.g. vim, emacs, nano, hx). Defaults to $EDITOR +wrap: no # Controls text wrapping (no, auto, ) +wrap_code: false # Enables or disables the wrapping of code blocks # ---- Prelude ---- -repl_prelude: null # Set a default session or role for REPL mode to use (e.g. role:, session:, :) -cmd_prelude: null # Set a default session or role for CMD mode to use (e.g. role:, session:, :) -agent_session: null # Set a session to use when starting an agent (e.g. temp, default) +repl_prelude: null # Set a default session or role for REPL mode to use (e.g. role:, session:, :) +cmd_prelude: null # Set a default session or role for CMD mode to use (e.g. role:, session:, :) +agent_session: null # Set a session to use when starting an agent (e.g. temp, default) # ---- Appearance ---- -highlight: true # Controls syntax highlighting -raw_markdown: false # When true, render markdown as raw text with syntax highlighting only. When false (default), transforms markdown syntax (headings, bold, lists, etc.) into styled terminal output -light_theme: false # Activates a light color theme when true. env: COYOTE_LIGHT_THEME +highlight: true # Controls syntax highlighting +raw_markdown: false # When true, render markdown as raw text with syntax highlighting only. When false (default), transforms markdown syntax (headings, bold, lists, etc.) into styled terminal output +light_theme: false # Activates a light color theme when true. env: COYOTE_LIGHT_THEME # ---- Miscellaneous ---- -user_agent: null # Set User-Agent HTTP header, use `auto` for coyote/ -save_shell_history: true # Whether to save shell execution command to the history file -sync_models_url: > # URL to sync model changes from +user_agent: null # Set User-Agent HTTP header, use `auto` for coyote/ +save_shell_history: true # Whether to save shell execution command to the history file +sync_models_url: > # URL to sync model changes from https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml # ---- REPL Prompt ---- # Custom REPL left/right prompts; see the [REPL Prompt Documentation](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt) for more information -left_prompt: '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} ' -right_prompt: '{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}' +left_prompt: + '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} ' +right_prompt: + '{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}' # ---- Vault ---- # See the [Vault documentation](https://github.com/Dark-Alex-17/coyote/wiki/Vault) for more information on the Coyote vault. @@ -41,7 +42,7 @@ right_prompt: '{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purpl # The secrets_provider tells Coyote where to read and write secrets referenced via {{SECRET_NAME}} syntax. # # Shorthand: set vault_password_file to enable the local provider with that password file. -vault_password_file: null # Path to a file containing the password for the Coyote vault (cannot be a secret template) +vault_password_file: null # Path to a file containing the password for the Coyote vault (cannot be a secret template) # # Explicit: set secrets_provider to one of the supported types below. When secrets_provider is set, # vault_password_file is ignored. Note: secrets_provider itself cannot use {{SECRET}} template syntax. @@ -81,39 +82,39 @@ vault_password_file: null # Path to a file containing the password for the Coyot # ---- Function Calling ---- # See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details -function_calling_support: true # Enables or disables function calling (Globally). -mapping_tools: # Alias for a tool or toolset +function_calling_support: true # Enables or disables function calling (Globally). +mapping_tools: # Alias for a tool or toolset fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep' -enabled_tools: - null # Which tools to enable by default. - # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. - # Example (list form): - # enabled_tools: - # - fs - # - web_search_coyote - # Example (comma-separated form): - # enabled_tools: fs,web_search_coyote -visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools') - # - ast_grep.sh - # - demo_py.py - # - demo_sh.sh - # - demo_ts.ts +enabled_tools: null # Which tools to enable by default. + # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. + # Example (list form): + # enabled_tools: + # - fs + # - web_search_coyote + # Example (comma-separated form): + # enabled_tools: fs,web_search_coyote +visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools'). + # Null/missing = all tools in the global tools dir are visible; [] = none. +# - ast_grep.sh +# - demo_py.py +# - demo_sh.sh +# - demo_ts.ts - execute_command.sh - # - execute_py_code.py - # - execute_sql_code.sh - # - fetch_url_via_curl.sh - # - fetch_url_via_jina.sh +# - execute_py_code.py +# - execute_sql_code.sh +# - fetch_url_via_curl.sh +# - fetch_url_via_jina.sh - fs_cat.sh - fs_ls.sh - # - fs_read.sh - # - fs_glob.sh - # - fs_grep.sh - # - fs_mkdir.sh - # - fs_patch.sh - # - fs_write.sh +# - fs_read.sh +# - fs_glob.sh +# - fs_grep.sh +# - fs_mkdir.sh +# - fs_patch.sh +# - fs_write.sh - get_current_time.sh - # - get_current_weather.py - # - get_current_weather.ts +# - get_current_weather.py +# - get_current_weather.ts - get_current_weather.sh # - search_arxiv.sh # - search_wikipedia.sh @@ -126,106 +127,97 @@ visible_tools: # Which tools are visible to be compiled (and are thus able to be # ---- MCP Servers ---- # See the [MCP Servers documentation](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) for more details -mcp_server_support: true # Enables or disables MCP servers (globally). -mapping_mcp_servers: # Alias for an MCP server or set of servers +mcp_server_support: true # Enables or disables MCP servers (globally). +mapping_mcp_servers: # Alias for an MCP server or set of servers git: github,gitmcp -enabled_mcp_servers: - null # Which MCP servers to enable by default. - # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. - # Example (list form): - # enabled_mcp_servers: - # - github - # - slack - # Example (comma-separated form): - # enabled_mcp_servers: github,slack,ddg-search -mcp_tools: - null # Per-server MCP tool allowlists (glob patterns: * and ? supported). - # Tools that match no pattern are hidden from the model as if they - # don't exist. Stacks with the other allowlist layers (mcp.json - # `allowedTools`, role, agent, session, skill, graph node). Every - # configured layer must allow a tool, so layers only ever narrow. - # An empty list blocks all of a server's tools. - # Example: - # mcp_tools: - # github: - # - get_* - # - list_* - # slack: [] -no_workspace_mcp: - false # Disable loading workspace-local MCP servers (default: false). - # When false (the default), Coyote merges the first workspace MCP config it finds - # into the global MCP registry at startup, checking in order: - # 1. .coyote/mcp.json - # 2. .coyote/.mcp.json (Claude-style file name) - # 3. .mcp.json (project root; Claude Code convention) - # Workspace entries shadow global ones on name collision. - # Set to true (or pass --no-workspace-mcp) to skip this entirely. +enabled_mcp_servers: null # Which MCP servers to enable by default. + # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. + # Example (list form): + # enabled_mcp_servers: + # - github + # - slack + # Example (comma-separated form): + # enabled_mcp_servers: github,slack,ddg-search +mcp_tools: null # Per-server MCP tool allowlists (glob patterns: * and ? supported). + # Tools that match no pattern are hidden from the model as if they + # don't exist. Stacks with the other allowlist layers (mcp.json + # `allowedTools`, role, agent, session, skill, graph node). Every + # configured layer must allow a tool, so layers only ever narrow. + # An empty list blocks all of a server's tools. + # Example: + # mcp_tools: + # github: + # - get_* + # - list_* + # slack: [] +no_workspace_mcp: false # Disable loading workspace-local MCP servers (default: false). + # When false (the default), Coyote merges the first workspace MCP config it finds + # into the global MCP registry at startup, checking in order: + # 1. .coyote/mcp.json + # 2. .coyote/.mcp.json (Claude-style file name) + # 3. .mcp.json (project root; Claude Code convention) + # Workspace entries shadow global ones on name collision. + # Set to true (or pass --no-workspace-mcp) to skip this entirely. # ---- Skills ---- # Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation. # See the [Skills documentation](https://github.com/Dark-Alex-17/coyote/wiki/Skills) for more details. -skills_enabled: - true # Master switch. Set to false to hide all skill management tools from the model. - # Skills also require `function_calling_support: true` above to work at all. -visible_skills: # The universe of skills allowed to be enabled in any context. Omit (null) for "all installed". +skills_enabled: true # Master switch. Set to false to hide all skill management tools from the model. + # Skills also require `function_calling_support: true` above to work at all. +visible_skills: # The universe of skills allowed to be enabled in any context. Omit (null) for "all installed". - ai-slop-remover - code-review - frontend-ui-ux - git-master -enabled_skills: - null # Which skills are available by default (no role/agent/session active). null = all visible. - # Accepts either a YAML list or a comma-separated string. - # Example (list form): - # enabled_skills: - # - git-master - # - ai-slop-remover - # Example (comma-separated form): - # enabled_skills: git-master,ai-slop-remover -inject_skill_instructions: - true # Inject a short hint pointing the model at `skill__list` when skills are enabled in - # this context. Only injected if `function_calling_support`, `skills_enabled`, and the - # effective enabled skill set is non-empty (default: true). -skill_instructions: null # Custom text used for the skill hint when injected. If null, uses built-in default. +enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible. + # Accepts either a YAML list or a comma-separated string. + # Example (list form): + # enabled_skills: + # - git-master + # - ai-slop-remover + # Example (comma-separated form): + # enabled_skills: git-master,ai-slop-remover +inject_skill_instructions: true # Inject a short hint pointing the model at `skill__list` when skills are enabled in + # this context. Only injected if `function_calling_support`, `skills_enabled`, and the + # effective enabled skill set is non-empty (default: true). +skill_instructions: null # Custom text used for the skill hint when injected. If null, uses built-in default. # ---- Macros ---- # Macros are Coyote's custom commands: named sequences of REPL commands and prompts, invoked directly by name # (a macro file named `review.yaml` runs as `.review [args]`; built-in commands always win a name collision). # Workspace-local macros in `.coyote/macros/` shadow same-named global macros (skip them with --no-workspace-macros). # See the [Macros documentation](https://github.com/Dark-Alex-17/coyote/wiki/Macros) for more details. -enabled_macros: - null # Which macros are invocable by default (no role/agent/session active). null = all visible. - # An empty list means NO macros are invocable. Accepts either a YAML list or a - # comma-separated string. Roles, agents, and sessions may define their own - # `enabled_macros`; the most specific active one wins (session > agent > role > global). - # Example (list form): - # enabled_macros: - # - generate-commit-message - # Example (comma-separated form): - # enabled_macros: generate-commit-message,review +enabled_macros: null # Which macros are invocable by default (no role/agent/session active). null = all visible. + # An empty list means NO macros are invocable. Accepts either a YAML list or a + # comma-separated string. Roles, agents, and sessions may define their own + # `enabled_macros`; the most specific active one wins (session > agent > role > global). + # Example (list form): + # enabled_macros: + # - generate-commit-message + # Example (comma-separated form): + # enabled_macros: generate-commit-message,review # ---- Auto-Continue (Todo System) ---- # The auto-continue system provides built-in task tracking for improved reliability. # When enabled, the model can create todo lists and the system will automatically # prompt it to continue when incomplete tasks remain. # See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information -auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false) -max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10) -inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true) -continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default +auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false) +max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10) +inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true) +continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default # ---- Session ---- # See the [Session documentation](https://github.com/Dark-Alex-17/coyote/wiki/Sessions) for more information -save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save save; if null, ask the user what to do -compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold -summarization_prompt: - > # The text prompt used for creating a concise summary of session message +save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save save; if null, ask the user what to do +compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold +summarization_prompt: > # The text prompt used for creating a concise summary of session message 'Summarize the discussion briefly in 200 words or less to use as a prompt for future context.' -summary_context_prompt: - > # The text prompt used for including the summary of the entire session as context to the model +summary_context_prompt: > # The text prompt used for including the summary of the entire session as context to the model 'This is a summary of the chat history as a recap: ' -compression_keep_last: 0 # Number of most-recent messages to keep visible after compression (0 = compress all messages) -max_tool_result_chars: null # Cap on tool result characters forwarded to the model per call (null = no cap) -max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once per context (default: 5; 0 disables background jobs entirely) +compression_keep_last: 0 # Number of most-recent messages to keep visible after compression (0 = compress all messages) +max_tool_result_chars: null # Cap on tool result characters forwarded to the model per call (null = no cap) +max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once per context (default: 5; 0 disables background jobs entirely) # ---- Memory ---- # See the [Memory documentation](https://github.com/Dark-Alex-17/coyote/wiki/Memory) for more information. @@ -234,13 +226,11 @@ max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once pe # even when memory files exist. The cascade is: agent > session > role > app. # Bootstrap with `coyote --init-memory [global|workspace]` to create the marker file # the LLM needs before it will write any memory. -memory: null # null = enabled when memory exists on disk; true = force on; false = force off -memory_cap_with_tools: - null # Char cap for injected memory when function calling is available (default: 6000). - # Only MEMORY.md indexes are injected; the LLM uses memory__read to fetch drill files. -memory_cap_without_tools: - null # Char cap when function calling is unavailable (default: 12000). - # Indexes plus drill file bodies are injected up to this cap. +memory: null # null = enabled when memory exists on disk; true = force on; false = force off +memory_cap_with_tools: null # Char cap for injected memory when function calling is available (default: 6000). + # Only MEMORY.md indexes are injected; the LLM uses memory__read to fetch drill files. +memory_cap_without_tools: null # Char cap when function calling is unavailable (default: 12000). + # Indexes plus drill file bodies are injected up to this cap. # ---- Workspace Instructions ---- # Human-curated project instructions injected read-only into the system prompt, in full. @@ -248,23 +238,22 @@ memory_cap_without_tools: # chain below (per directory, in order). Scaffold with `coyote --init-instructions`. # Disable per-invocation with --no-workspace-instructions, or override the chain with # repeatable --workspace-instructions-file flags. -workspace_instructions: null # null/true = inject when an instructions file exists; false = never inject -workspace_instructions_files: - null # File name chain to search, in priority order. - # Default: [COYOTE.md, AGENTS.md, CLAUDE.md, GEMINI.md] - # Set to a custom list to reorder or drop fallbacks, e.g.: - # workspace_instructions_files: [COYOTE.md] +workspace_instructions: null # null/true = inject when an instructions file exists; false = never inject +workspace_instructions_files: null # File name chain to search, in priority order. + # Default: [COYOTE.md, AGENTS.md, CLAUDE.md, GEMINI.md] + # Set to a custom list to reorder or drop fallbacks, e.g.: + # workspace_instructions_files: [COYOTE.md] # ---- RAG ---- # See the [RAG Docs](https://github.com/Dark-Alex-17/coyote/wiki/RAG) for more details. -rag_embedding_model: null # Specifies the embedding model used for context retrieval -rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Coyote uses Reciprocal Rank Fusion by default -rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries -rag_chunk_size: null # Defines the size of chunks for document processing in characters -rag_chunk_overlap: null # Defines the overlap between chunks -rag_extractor_model: null # LLM model for graph-based entity/relationship extraction; when set, enables a graph RAG signal alongside vector and BM25 -rag_extractor_prompt: null # Custom extraction prompt template; must contain __CHUNK__ placeholder; defaults to built-in prompt when null -rag_graph_hops: 1 # Number of hops to expand from matched entities at query time (0 = seed nodes only; 1 = direct neighbors; increase for denser graphs) +rag_embedding_model: null # Specifies the embedding model used for context retrieval +rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Coyote uses Reciprocal Rank Fusion by default +rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries +rag_chunk_size: null # Defines the size of chunks for document processing in characters +rag_chunk_overlap: null # Defines the overlap between chunks +rag_extractor_model: null # LLM model for graph-based entity/relationship extraction; when set, enables a graph RAG signal alongside vector and BM25 +rag_extractor_prompt: null # Custom extraction prompt template; must contain __CHUNK__ placeholder; defaults to built-in prompt when null +rag_graph_hops: 1 # Number of hops to expand from matched entities at query time (0 = seed nodes only; 1 = direct neighbors; increase for denser graphs) # Defines the query structure using variables like __CONTEXT__, __SOURCES__, and __INPUT__ to tailor searches to specific needs rag_template: | Answer the query based on the context while respecting the rules. (user query, some textual context and rules, all inside xml tags) @@ -295,14 +284,13 @@ document_loaders: # You can add custom loaders using the following syntax: # : # Note: Use `$1` for input file and `$2` for output file. If `$2` is omitted, use stdout as output. - pdf: 'pdftotext $1 -' # Use pdftotext to convert a PDF file to text + pdf: 'pdftotext $1 -' # Use pdftotext to convert a PDF file to text # (see https://poppler.freedesktop.org for details on how to install pdftotext) - docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text + docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text # (see https://pandoc.org for details on how to install pandoc) - jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text; + jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text; # Requires a Jina API key to be added to the Coyote vault - git: - > # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek) + git: > # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek) sh -c "yek $1 --json | jq 'map({ path: .filename, contents: .content })'" # ---- Clients ---- @@ -318,10 +306,10 @@ clients: # supports_function_calling: true # - name: xxxx # Embedding model # type: embedding - # default_chunk_size: 1500 + # default_chunk_size: 1500 # max_batch_size: 100 # - name: xxxx # Reranker model - # type: reranker + # type: reranker # patch: # Patch API calls # chat_completions: # API type; Possible values: chat_completions, embeddings, and rerank # : # The regex to match model names, e.g. '.*' 'gpt-4o' 'gpt-4o|gpt-4-.*' @@ -337,15 +325,15 @@ clients: # See https://platform.openai.com/docs/quickstart - type: openai - api_base: https://api.openai.com/v1 # Optional - api_key: '{{OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault - organization_id: org-xxx # Optional + api_base: https://api.openai.com/v1 # Optional + api_key: '{{OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + organization_id: org-xxx # Optional # For any platform compatible with OpenAI's API - type: openai-compatible name: ollama api_base: http://localhost:11434/v1 - api_key: '{{OLLAMA_API_KEY}}' # Optional; You can either hard-code or inject secrets from the Coyote vault + api_key: '{{OLLAMA_API_KEY}}' # Optional; You can either hard-code or inject secrets from the Coyote vault models: - name: deepseek-r1 max_input_tokens: 131072 @@ -363,10 +351,9 @@ clients: # See https://ai.google.dev/docs - type: gemini api_base: https://generativelanguage.googleapis.com/v1beta - api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault - auth: - null # When set to 'oauth', Coyote will use OAuth instead of an API key - # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL + api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key + # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL patch: chat_completions: '.*': @@ -383,27 +370,25 @@ clients: # See https://docs.anthropic.com/claude/reference/getting-started-with-the-api - type: claude - api_base: https://api.anthropic.com/v1 # Optional - api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault - auth: - null # When set to 'oauth', Coyote will use OAuth instead of an API key - # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL + api_base: https://api.anthropic.com/v1 # Optional + api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key + # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL # See https://docs.mistral.ai/ - type: openai-compatible name: mistral api_base: https://api.mistral.ai/v1 - api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://docs.x.ai/docs - OAuth via SuperGrok / X Premium+ subscription - type: openai-compatible name: xai api_base: https://api.x.ai/v1 - api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault - auth: - null # When set to 'oauth', Coyote will use OAuth instead of an API key - # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL - # Note: Oauth requires SuperGrok/X Premium+ subscription + api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key + # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL + # Note: Oauth requires SuperGrok/X Premium+ subscription # Example: private OpenAI-compatible gateway with client_credentials OAuth # - type: openai-compatible @@ -433,31 +418,31 @@ clients: - type: openai-compatible name: ai12 api_base: https://api.ai21.com/studio/v1 - api_key: '{{AI21_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{AI21_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://docs.cohere.com/docs/the-cohere-platform - type: cohere - api_base: https://api.cohere.ai/v2 # Optional - api_key: '{{COHERE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_base: https://api.cohere.ai/v2 # Optional + api_key: '{{COHERE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://docs.perplexity.ai/getting-started/overview - type: openai-compatible name: perplexity api_base: https://api.perplexity.ai - api_key: '{{PERPLEXITY_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{PERPLEXITY_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://console.groq.com/docs/quickstart - type: openai-compatible name: groq api_base: https://api.groq.com/openai/v1 - api_key: '{{GROQ_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{GROQ_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart - type: azure-openai api_base: https://{RESOURCE}.openai.azure.com - api_key: '{{AZURE_OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{AZURE_OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault models: - - name: gpt-4o # Model deployment name + - name: gpt-4o # Model deployment name max_input_tokens: 128000 supports_vision: true supports_function_calling: true @@ -469,7 +454,7 @@ clients: # Specifies an application default credentials (adc) file # Run `gcloud auth application-default login` to initialize the ADC file # see https://cloud.google.com/docs/authentication/external/set-up-adc for more information - adc_file: /application_default_credentials.json # Optional + adc_file: /application_default_credentials.json # Optional patch: chat_completions: 'gemini-.*': @@ -486,76 +471,77 @@ clients: # See https://docs.aws.amazon.com/bedrock/latest/userguide/ - type: bedrock - access_key_id: '{{AWS_ACCESS_KEY_ID}}' # You can either hard-code or inject secrets from the Coyote vault - secret_access_key: '{{AWS_SECRET_ACCESS_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + access_key_id: '{{AWS_ACCESS_KEY_ID}}' # You can either hard-code or inject secrets from the Coyote vault + secret_access_key: '{{AWS_SECRET_ACCESS_KEY}}' # You can either hard-code or inject secrets from the Coyote vault region: xxx - session_token: xxx # Optional, only needed for temporary credentials + session_token: xxx # Optional, only needed for temporary credentials # See https://developers.cloudflare.com/workers-ai/ - type: openai-compatible name: cloudflare api_base: https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/v1 - api_key: '{{CLOUDFLARE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{CLOUDFLARE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html - type: openai-compatible name: ernie api_base: https://qianfan.baidubce.com/v2 - api_key: '{{BAIDU_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{BAIDU_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://dashscope.aliyun.com/ - type: openai-compatible name: qianwen api_base: https://dashscope.aliyuncs.com/compatible-mode/v1 - api_key: '{{ALIYUN_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{ALIYUN_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://cloud.tencent.com/product/hunyuan - type: openai-compatible name: hunyuan api_base: https://api.hunyuan.cloud.tencent.com/v1 - api_key: '{{TENCENT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{TENCENT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://platform.moonshot.cn/docs/intro - type: openai-compatible name: moonshot api_base: https://api.moonshot.cn/v1 - api_key: '{{MOONSHOT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{MOONSHOT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://platform.deepseek.com/api-docs/ - type: openai-compatible name: deepseek api_base: https://api.deepseek.com - api_key: '{{DEEPSEEK_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{DEEPSEEK_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://open.bigmodel.cn/dev/howuse/introduction - type: openai-compatible name: zhipuai api_base: https://open.bigmodel.cn/api/paas/v4 - api_key: '{{ZHIPUAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{ZHIPUAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://platform.minimaxi.com/document/Fast%20access - type: openai-compatible name: minimax api_base: https://api.minimax.chat/v1 - api_key: '{{MINIMAX_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{MINIMAX_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://openrouter.ai/docs#quick-start - type: openai-compatible name: openrouter api_base: https://openrouter.ai/api/v1 - api_key: '{{OPENROUTER_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{OPENROUTER_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://github.com/marketplace/models - type: openai-compatible name: github api_base: https://models.inference.ai.azure.com - api_key: '{{GITHUB_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{GITHUB_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://deepinfra.com/docs - type: openai-compatible name: deepinfra api_base: https://api.deepinfra.com/v1/openai - api_key: '{{DEEPINFRA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{DEEPINFRA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + # ----- RAG dedicated ----- @@ -563,10 +549,10 @@ clients: - type: openai-compatible name: jina api_base: https://api.jina.ai/v1 - api_key: '{{JINA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{JINA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault # See https://docs.voyageai.com/docs/introduction - type: openai-compatible name: voyageai api_base: https://api.voyageai.com/v1 - api_key: '{{VOYAGEAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault + api_key: '{{VOYAGEAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault \ No newline at end of file diff --git a/src/config/app_state.rs b/src/config/app_state.rs index 5cbd452..ebe65aa 100644 --- a/src/config/app_state.rs +++ b/src/config/app_state.rs @@ -69,7 +69,7 @@ impl AppState { } } - let mut functions = Functions::init(config.visible_tools.as_ref().unwrap_or(&Vec::new()))?; + let mut functions = Functions::init(config.visible_tools.as_deref())?; if !mcp_registry.is_empty() && config.mcp_server_support { functions.append_mcp_meta_functions(mcp_registry.server_features()); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 26b2eca..bc95e9f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -150,6 +150,9 @@ fn validate_no_template_in_secrets_provider(content: &str) -> Result<()> { const DARK_THEME: &[u8] = include_bytes!("../../assets/monokai-extended.theme.bin"); const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.theme.bin"); +/// Fully documented config skeleton the first-run wizard splices dynamic values into. +const CONFIG_TEMPLATE: &str = include_str!("../../assets/config-template.yaml"); + const CONFIG_FILE_NAME: &str = "config.yaml"; const AGENT_GRAPH_FILE_NAME: &str = "graph.yaml"; const ROLES_DIR_NAME: &str = "roles"; @@ -180,27 +183,6 @@ const BUNDLE_MANIFEST_FILE: &str = "coyote-bundle.yaml"; const SBX_MIXIN_KITS_DIR_NAME: &str = "sbx-mixin-kits"; const GIT_DIR_NAME: &str = ".git"; const GITIGNORE_FILE_NAME: &str = ".gitignore"; -const DEFAULT_VISIBLE_TOOLS: [&str; 19] = [ - "execute_command.sh", - "execute_py_code.py", - "execute_sql_code.sh", - "fetch_url_via_curl.sh", - "fs_cat.sh", - "fs_glob.sh", - "fs_grep.sh", - "fs_ls.sh", - "fs_mkdir.sh", - "fs_patch.sh", - "fs_read.sh", - "fs_rm.sh", - "fs_write.sh", - "ast_grep.sh", - "get_current_time.sh", - "get_current_weather.sh", - "search_wikipedia.sh", - "search_arxiv.sh", - "web_search_coyote.sh", -]; const CLIENTS_FIELD: &str = "clients"; @@ -811,51 +793,20 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> { let client = Select::new("API Provider (required):", list_client_types()).prompt()?; - let mut config = json!({}); let (model, clients_config) = create_client_config(client, &vault).await?; - config["model"] = model.into(); - match &provider_choice { - None => { - config["vault_password_file"] = - vault.local_password_file()?.display().to_string().into(); - } + let secrets = match &provider_choice { + None => json!({ + "vault_password_file": vault.local_password_file()?.display().to_string() + }), Some(provider) => { - config["secrets_provider"] = serde_json::to_value(provider) + let provider = serde_json::to_value(provider) .with_context(|| "failed to serialize secrets_provider config")?; + json!({ "secrets_provider": provider }) } - } - config["stream"] = json!(true); - config["save"] = json!(true); - config["keybindings"] = json!("vi"); - config["wrap"] = json!("auto"); - config["wrap_code"] = json!(false); - config["function_calling_support"] = json!(true); - config["enabled_tools"] = json!(null); - config["visible_tools"] = json!(DEFAULT_VISIBLE_TOOLS); - config["mcp_server_support"] = json!(true); - config["enabled_mcp_servers"] = json!(null); - config["highlight"] = json!(true); - config["light_theme"] = json!(false); - config[CLIENTS_FIELD] = clients_config; + }; - let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?; - let config_data = format!( - "# see https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml\n\n{config_data}" - ); - - ensure_parent_exists(config_path)?; - std::fs::write(config_path, config_data) - .with_context(|| format!("Failed to write to '{}'", config_path.display()))?; - #[cfg(unix)] - { - use std::os::unix::prelude::PermissionsExt; - let perms = std::fs::Permissions::from_mode(0o600); - std::fs::set_permissions(config_path, perms)?; - } - - println!("✓ Saved the config file to '{}'.\n", config_path.display()); - - Ok(()) + let config_data = render_config_template(&model, Some(&secrets), &clients_config)?; + write_config_file(config_path, &config_data) } async fn create_config_file_sandbox(config_path: &Path) -> Result<()> { @@ -865,7 +816,7 @@ async fn create_config_file_sandbox(config_path: &Path) -> Result<()> { "Running in sandbox mode — your API provider credentials are managed by your host Coyote configuration if configured." ); - let oai_api_base = client::OPENAI_COMPATIBLE_PROVIDERS + let oai_api_base = OPENAI_COMPATIBLE_PROVIDERS .iter() .find(|(name, _)| *name == client) .map(|(_, url)| *url); @@ -878,13 +829,13 @@ async fn create_config_file_sandbox(config_path: &Path) -> Result<()> { } else { api_base.to_string() }; - serde_json::json!({ + json!({ "type": "openai-compatible", "name": client, "api_base": api_base_str, }) } else { - serde_json::json!({ "type": client }) + json!({ "type": client }) }; if client::client_type_supports_oauth(client) { @@ -898,27 +849,35 @@ async fn create_config_file_sandbox(config_path: &Path) -> Result<()> { let model = set_client_models_config(&mut client_config, client).await?; - let mut config = serde_json::json!({}); - config["model"] = model.into(); - config["stream"] = serde_json::json!(true); - config["save"] = serde_json::json!(true); - config["keybindings"] = serde_json::json!("vi"); - config["wrap"] = serde_json::json!("auto"); - config["wrap_code"] = serde_json::json!(false); - config["function_calling_support"] = serde_json::json!(true); - config["enabled_tools"] = serde_json::json!(null); - config["visible_tools"] = serde_json::json!(DEFAULT_VISIBLE_TOOLS); - config["mcp_server_support"] = serde_json::json!(true); - config["enabled_mcp_servers"] = serde_json::json!(null); - config["highlight"] = serde_json::json!(true); - config["light_theme"] = serde_json::json!(false); - config[CLIENTS_FIELD] = serde_json::json!(vec![client_config]); + let config_data = render_config_template(&model, None, &json!([client_config]))?; + write_config_file(config_path, &config_data) +} - let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?; - let config_data = format!( - "# see https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml\n\n{config_data}" - ); +fn render_config_template( + model: &str, + secrets: Option<&serde_json::Value>, + clients: &serde_json::Value, +) -> Result { + let to_yaml = |value: &serde_json::Value| { + serde_yaml::to_string(value).with_context(|| "Failed to create config") + }; + let model_block = to_yaml(&json!({ "model": model }))?; + let secrets_block = match secrets { + Some(value) => to_yaml(value)?, + None => "# Sandbox mode: no vault provider is configured; secrets are provisioned\n\ + # from the host when the sandbox is created.\n" + .to_string(), + }; + let clients_block = to_yaml(&json!({ CLIENTS_FIELD: clients }))?; + + Ok(CONFIG_TEMPLATE + .replacen("__MODEL_BLOCK__\n", &model_block, 1) + .replacen("__SECRETS_BLOCK__\n", &secrets_block, 1) + .replacen("__CLIENTS_BLOCK__\n", &clients_block, 1)) +} + +fn write_config_file(config_path: &Path, config_data: &str) -> Result<()> { ensure_parent_exists(config_path)?; std::fs::write(config_path, config_data) .with_context(|| format!("Failed to write to '{}'", config_path.display()))?; @@ -1174,6 +1133,55 @@ clients: assert_eq!(cfg.enabled_macros, None); } + #[test] + fn config_template_renders_parseable_config() { + let secrets = json!({ "vault_password_file": "/home/user/.coyote_password" }); + let clients = json!([{ "type": "openai", "api_key": "sk-test" }]); + + let rendered = render_config_template("openai:gpt-4o", Some(&secrets), &clients).unwrap(); + + assert!(!rendered.contains("__MODEL_BLOCK__")); + assert!(!rendered.contains("__SECRETS_BLOCK__")); + assert!(!rendered.contains("__CLIENTS_BLOCK__")); + + let cfg = Config::load_from_str(&rendered).unwrap(); + assert_eq!(cfg.model_id, "openai:gpt-4o"); + assert_eq!( + cfg.vault_password_file, + Some(PathBuf::from("/home/user/.coyote_password")) + ); + assert!(cfg.secrets_provider.is_none()); + assert_eq!(cfg.keybindings, "emacs"); + assert!(cfg.save); + assert_eq!(cfg.wrap.as_deref(), Some("auto")); + assert!(cfg.visible_tools.is_none()); + assert!(cfg.mapping_tools.is_empty()); + assert!(cfg.document_loaders.is_empty()); + assert_eq!(cfg.compression_threshold, 4000); + assert!(cfg.theme.is_none()); + assert_eq!(cfg.clients.len(), 1); + } + + #[test] + fn config_template_renders_parseable_sandbox_config() { + let clients = json!([{ "type": "claude" }]); + + let rendered = + render_config_template("claude:claude-sonnet-4-20250514", None, &clients).unwrap(); + + assert!(!rendered.contains("__SECRETS_BLOCK__")); + + let cfg = Config::load_from_str(&rendered).unwrap(); + assert_eq!(cfg.model_id, "claude:claude-sonnet-4-20250514"); + assert!(cfg.vault_password_file.is_none()); + assert!(cfg.secrets_provider.is_none()); + assert_eq!(cfg.keybindings, "emacs"); + assert!(cfg.save); + assert!(cfg.visible_tools.is_none()); + assert_eq!(cfg.compression_threshold, 4000); + assert_eq!(cfg.clients.len(), 1); + } + #[test] fn config_enabled_macros_empty_string_is_some_empty() { let cfg: Config = serde_yaml::from_str("enabled_macros: \"\"").unwrap(); diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 55d0972..c027fd4 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -4266,7 +4266,7 @@ impl RequestContext { } } - let mut functions = Functions::init(app.visible_tools.as_ref().unwrap_or(&Vec::new()))?; + let mut functions = Functions::init(app.visible_tools.as_deref())?; if self.working_mode.is_repl() { functions.append_user_interaction_functions(); } @@ -4687,7 +4687,7 @@ impl RequestContext { pub fn exit_agent(&mut self, app: &AppConfig) -> Result<()> { self.exit_session()?; - let mut functions = Functions::init(app.visible_tools.as_ref().unwrap_or(&Vec::new()))?; + let mut functions = Functions::init(app.visible_tools.as_deref())?; if self.working_mode.is_repl() { functions.append_user_interaction_functions(); } diff --git a/src/function/mod.rs b/src/function/mod.rs index 19dd0b2..8da65f7 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -214,6 +214,57 @@ fn tool_source_stems() -> Result> { Ok(stems) } +fn all_tool_source_files() -> Result> { + let tools_dir = paths::global_tools_dir(); + if !tools_dir.exists() { + return Ok(Vec::new()); + } + + let mut file_names = Vec::new(); + for entry in fs::read_dir(&tools_dir)? { + let path = entry?.path(); + if path.is_file() + && let Some(name) = path.file_name().and_then(OsStr::to_str) + { + file_names.push(name.to_string()); + } + } + + Ok(dedupe_tool_files_by_stem(file_names)) +} + +fn dedupe_tool_files_by_stem(file_names: Vec) -> Vec { + fn extension_rank(name: &str) -> Option { + let ext = Path::new(name).extension().and_then(OsStr::to_str)?; + match Language::from_extension(ext) { + Language::Bash => Some(0), + Language::Python => Some(1), + Language::TypeScript => Some(2), + Language::Unsupported => None, + } + } + + let mut best: HashMap = HashMap::new(); + for name in file_names { + let Some(rank) = extension_rank(&name) else { + continue; + }; + let Some(stem) = Path::new(&name).file_stem().and_then(OsStr::to_str) else { + continue; + }; + match best.get(stem) { + Some((best_rank, _)) if *best_rank <= rank => {} + _ => { + best.insert(stem.to_string(), (rank, name)); + } + } + } + + let mut files: Vec = best.into_values().map(|(_, name)| name).collect(); + files.sort(); + files +} + fn bin_entry_stem(file_name: &str) -> &str { let name = file_name.strip_prefix("run-").unwrap_or(file_name); Path::new(name) @@ -587,18 +638,23 @@ impl Functions { Ok(()) } - pub fn init(visible_tools: &[String]) -> Result { + pub fn init(visible_tools: Option<&[String]>) -> Result { Self::remove_stale_global_function_binaries()?; + let (visible_tools, lenient) = match visible_tools { + Some(tools) => (tools.to_vec(), false), + None => (all_tool_source_files()?, true), + }; + let declarations = Self { - declarations: Self::build_global_tool_declarations(visible_tools)?, + declarations: Self::build_global_tool_declarations(&visible_tools, lenient)?, }; info!( "Building global function binaries in {}", paths::functions_bin_dir().display() ); - Self::build_global_function_binaries(visible_tools, None)?; + Self::build_global_function_binaries(&visible_tools, None, lenient)?; Ok(declarations) } @@ -608,13 +664,13 @@ impl Functions { let global_tools_declarations = if !global_tools.is_empty() { info!("Loading global tools for agent: {name}: {global_tools:?}"); - let tools_declarations = Self::build_global_tool_declarations(global_tools)?; + let tools_declarations = Self::build_global_tool_declarations(global_tools, false)?; info!( "Building global function binaries required by agent: {name} in {}", paths::functions_bin_dir().display() ); - Self::build_global_function_binaries(global_tools, Some(name))?; + Self::build_global_function_binaries(global_tools, Some(name), false)?; tools_declarations } else { debug!("No global tools found for agent: {}", name); @@ -964,13 +1020,17 @@ impl Functions { fn build_global_tool_declarations( enabled_tools: &[String], + lenient: bool, ) -> Result> { let global_tools_directory = paths::global_tools_dir(); let mut function_declarations = Vec::new(); for tool in enabled_tools { - let declaration = Self::generate_declarations(&global_tools_directory.join(tool))?; - function_declarations.extend(declaration); + match Self::generate_declarations(&global_tools_directory.join(tool)) { + Ok(declaration) => function_declarations.extend(declaration), + Err(err) if lenient => warn!("Skipping tool {tool}: {err}"), + Err(err) => return Err(err), + } } Ok(function_declarations) @@ -1032,41 +1092,50 @@ impl Functions { fn build_global_function_binaries( enabled_tools: &[String], agent_name: Option<&str>, + lenient: bool, ) -> Result<()> { for tool in enabled_tools { - let language = Language::from( - &Path::new(&tool) - .extension() - .and_then(OsStr::to_str) - .map(|s| s.to_lowercase()) - .ok_or_else(|| { - anyhow::format_err!("Unable to extract file extension from path: {tool:?}") - })?, - ); - let binary_name = Path::new(&tool) - .file_stem() - .and_then(OsStr::to_str) - .ok_or_else(|| { - anyhow::format_err!("Unable to extract file name from path: {tool:?}") - })?; - - if language == Language::Unsupported { - bail!("Unsupported tool file extension: {}", language.as_ref()); + match Self::build_global_function_binary(tool, agent_name) { + Ok(()) => {} + Err(err) if lenient => warn!("Skipping binary for tool {tool}: {err}"), + Err(err) => return Err(err), } - - let tool_path = paths::global_tools_dir().join(tool); - let custom_runtime = extract_shebang_runtime(&tool_path); - Self::build_binaries( - binary_name, - language, - BinaryType::Tool(agent_name), - custom_runtime.as_deref(), - )?; } Ok(()) } + fn build_global_function_binary(tool: &str, agent_name: Option<&str>) -> Result<()> { + let language = Language::from( + &Path::new(tool) + .extension() + .and_then(OsStr::to_str) + .map(|s| s.to_lowercase()) + .ok_or_else(|| { + anyhow::format_err!("Unable to extract file extension from path: {tool:?}") + })?, + ); + let binary_name = Path::new(tool) + .file_stem() + .and_then(OsStr::to_str) + .ok_or_else(|| { + anyhow::format_err!("Unable to extract file name from path: {tool:?}") + })?; + + if language == Language::Unsupported { + bail!("Unsupported tool file extension: {}", language.as_ref()); + } + + let tool_path = paths::global_tools_dir().join(tool); + let custom_runtime = extract_shebang_runtime(&tool_path); + Self::build_binaries( + binary_name, + language, + BinaryType::Tool(agent_name), + custom_runtime.as_deref(), + ) + } + fn remove_stale_agent_bin_entries(name: &str) -> Result<()> { let agent_bin_directory = paths::agent_bin_dir(name); @@ -2603,6 +2672,35 @@ mod tests { use std::sync::Arc; use std::{mem, process}; + #[test] + fn dedupe_tool_files_prefers_sh_over_py_over_ts() { + let files = vec![ + "get_current_weather.ts".to_string(), + "get_current_weather.py".to_string(), + "get_current_weather.sh".to_string(), + "fetch.ts".to_string(), + "fetch.py".to_string(), + "demo_ts.ts".to_string(), + ]; + + assert_eq!( + dedupe_tool_files_by_stem(files), + vec!["demo_ts.ts", "fetch.py", "get_current_weather.sh"] + ); + } + + #[test] + fn dedupe_tool_files_skips_unsupported_extensions() { + let files = vec![ + "notes.md".to_string(), + "tool.sh".to_string(), + "README".to_string(), + "archive.tar.gz".to_string(), + ]; + + assert_eq!(dedupe_tool_files_by_stem(files), vec!["tool.sh"]); + } + fn call(name: &str, id: Option<&str>) -> ToolCall { ToolCall::new(name.to_string(), json!({}), id.map(|s| s.to_string())) }