Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Added `PromptAssembly` to unify MainChat and sub-agent system prompt and tool spec assembly; removed legacy `LlmSession` prompt filtering APIs.
- Moved per-tool routing guidance into tool descriptions; tool usage remains in tool prompts. Oracle delegates tool choice to descriptions (no duplicated routing tables). If you customize `~/.config/aish/prompts/oracle.md`, remove duplicated tool-selection sections that mirror tool descriptions.
- Added explicit routing between `Agent(subagent_type=plan)` and `enter_plan_mode`; `enter_plan_mode` keeps routing in its description and usage-only text in its prompt appendix.
- Expanded built-in sub-agent system prompts (explore/plan/general-purpose) with read-only rules and efficient search strategy; Agent `prompt` schema now requires scope and thoroughness.
- Converted embedded LLM prompt templates (`oracle`, `cmd_error`, `failure_diagnose`) to English-only; SSH error-correction context injection is English as well. Removed unused embedded templates (`error_detect`, `system_diagnose`, `guess_command`) and stale copies under `crates/aish-shell/prompts/` (runtime source: `aish-prompts/src/manager.rs`).

## [0.3.6] - 2026-07-03

### Added
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/aish-llm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ langfuse-ergonomic.workspace = true

[dev-dependencies]
tempfile.workspace = true
aish-tools.workspace = true
19 changes: 9 additions & 10 deletions crates/aish-llm/src/agent.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
//! ReAct-style agent system for iterative reasoning and tool use.
//!
//! The agent follows the Thought/Action/Observation/Final Answer pattern:
//! 1. Send the query plus conversation history to the LLM.
//! 2. Parse the LLM response for structured ReAct blocks.
//! 3. If the response contains an **Action**, execute the named tool and feed
//! the result back as an **Observation**.
//! 4. If the response contains a **Final Answer**, return it immediately.
//! 5. Repeat until a final answer is produced or the iteration limit is hit.
//! **Legacy path:** used only by [`crate::diagnose_agent::DiagnoseAgent`] and
//! `system_diagnose` tooling. Main shell chat and sub-agent spawn use native
//! tool calling via [`crate::prompt::PromptAssembly`].

use aish_core::{AishError, LlmEvent, LlmEventType};
use tracing::{debug, info, warn};

use crate::client::LlmResponse;
use crate::prompt::{PromptAssembly, PromptContext};
use crate::streaming::StreamParser;
use crate::types::*;
use crate::LlmSession;
Expand Down Expand Up @@ -218,6 +215,8 @@ Available tools will be provided via the standard tool-calling interface.";

/// A ReAct-style agent that drives an [`LlmSession`] through iterative
/// Thought → Action → Observation cycles until a Final Answer is produced.
///
/// Legacy: prefer [`PromptAssembly`] + native tool calling for new code paths.
pub struct ReActAgent<'a> {
session: &'a LlmSession,
config: AgentConfig,
Expand Down Expand Up @@ -258,11 +257,11 @@ impl<'a> ReActAgent<'a> {
let _op_end = EmitOpEnd(self.session);

let mut messages: Vec<ChatMessage> = Vec::new();
let system_prompt = self.session.system_prompt_with_tool_prompts(system_prompt);
messages.push(ChatMessage::system(&system_prompt));
let bundle = PromptAssembly::build(self.session, PromptContext::MainChat, system_prompt);
messages.push(ChatMessage::system(&bundle.system_message));
messages.push(ChatMessage::user(query));

let tool_specs: Vec<ToolSpec> = self.session.tool_specs();
let tool_specs = bundle.tool_specs;
let has_tools = !tool_specs.is_empty();

for iteration in 0..self.config.max_iterations {
Expand Down
46 changes: 46 additions & 0 deletions crates/aish-llm/src/agents/builtin_prompts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! System prompts for built-in sub-agents.

pub const EXPLORE_SYSTEM_PROMPT: &str = "\
You are a read-only explore sub-agent for shell and ops investigation.

=== READ-ONLY MODE ===
You must NOT create, modify, delete, or move files. Do not spawn nested agents. Use only \
your allowed tools.

=== Tool selection ===
- glob: enumerate paths by pattern (prefer one broad recursive pattern per search root)
- grep: search file contents when you know what text to look for
- read_file: read a known path (use offset/limit for large files)
- bash: read-only discovery only (find, ls, stat, systemctl status, git log/diff, cat when \
read_file is unsuitable). Never use bash for writes, installs, or service changes.

=== Search strategy ===
- Start from the scope and thoroughness in the task brief. Default to quick unless told otherwise.
- quick: known locations and one or two broad patterns; stop when core paths are found.
- medium: expand to related config trees; avoid scanning entire filesystems.
- thorough: wider coverage but still batch with broad patterns, not dozens of narrow globs.
- Prefer one broad glob (e.g. /etc/**/*ssh*) or one find command over many per-directory globs.
- Parallel tool calls are fine when searches are independent; do not repeat the same pattern.
- For common ops layouts, check likely roots first (/etc, /var/log, systemd units) before /.

Return a concise conclusion listing findings. Do not dump full file contents unless essential.";

pub const PLAN_SYSTEM_PROMPT: &str = "\
You are a read-only planning sub-agent for shell and ops work.

=== READ-ONLY MODE ===
Your job is to analyze and produce a plan, runbook, or design advice in your final message \
only. You must NOT modify files, write plan artifacts, enter plan mode, or spawn nested agents.

You may use read-only tools to inspect the environment when that improves the plan.

Structure the conclusion clearly (steps, risks, prerequisites). Do not execute the plan.";

pub const GENERAL_PURPOSE_SYSTEM_PROMPT: &str = "\
You are a general-purpose sub-agent delegated a focused task from the parent session.

Use your available tools to complete the task. Do not spawn nested agents. Return a concise \
conclusion with outcomes the parent can relay to the user.

If the task is purely read-only exploration across many paths, prefer completing with efficient \
search rather than exhaustive brute-force tool spam.";
1 change: 1 addition & 0 deletions crates/aish-llm/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Issue #331: explore vertical slice — registry, tool filter, spawn_builtin.
//! Issue #332: plan + general-purpose built-ins and tool filtering.

mod builtin_prompts;
mod event_metadata;
mod mock_llm;
mod outcome;
Expand Down
38 changes: 32 additions & 6 deletions crates/aish-llm/src/agents/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

use std::collections::HashMap;

use super::builtin_prompts::{
EXPLORE_SYSTEM_PROMPT, GENERAL_PURPOSE_SYSTEM_PROMPT, PLAN_SYSTEM_PROMPT,
};

/// Read-only tool allowlist shared by `explore` and `plan` built-ins.
pub const READ_ONLY_ALLOWLIST: &[&str] = &["grep", "glob", "read_file", "bash"];

Expand Down Expand Up @@ -35,8 +39,10 @@ impl AgentDefinition {
pub fn explore() -> Self {
Self {
subagent_type: "explore".to_string(),
when_to_use: "Search logs, configs, services, network state, or code locations; read-only investigation.".to_string(),
system_prompt: "You are a read-only explore sub-agent for shell operations. Investigate using only your allowed tools. Do not modify files or spawn nested agents. Return a concise conclusion.".to_string(),
when_to_use: "Read-only investigation across logs, configs, services, or paths. \
Use for open-ended search; specify thoroughness in the Agent prompt: quick, medium, or thorough."
.to_string(),
system_prompt: EXPLORE_SYSTEM_PROMPT.to_string(),
max_turns: 15,
tool_strategy: ToolStrategy::Allowlist(read_only_allowlist()),
}
Expand All @@ -45,8 +51,11 @@ impl AgentDefinition {
pub fn plan() -> Self {
Self {
subagent_type: "plan".to_string(),
when_to_use: "Design changes, runbooks, or implementation plans; read-only planning.".to_string(),
system_prompt: "You are a read-only planning sub-agent for shell operations. Analyze and propose plans using only your allowed tools. Do not modify files, enter plan mode, or spawn nested agents. Return a concise plan or recommendation.".to_string(),
when_to_use:
"Read-only architect for implementation plans, runbooks, or design advice \
returned as one conclusion — NOT for enter_plan_mode or writing `.aish/plans/` files."
.to_string(),
system_prompt: PLAN_SYSTEM_PROMPT.to_string(),
max_turns: 20,
tool_strategy: ToolStrategy::Allowlist(read_only_allowlist()),
}
Expand All @@ -55,8 +64,10 @@ impl AgentDefinition {
pub fn general_purpose() -> Self {
Self {
subagent_type: "general-purpose".to_string(),
when_to_use: "General sub-tasks that need the parent's tools without nested Agent delegation.".to_string(),
system_prompt: "You are a general-purpose sub-agent. Complete the delegated task using your available tools. Do not spawn nested agents. Return a concise conclusion.".to_string(),
when_to_use: "Focused sub-tasks that need the parent's tool pool (including writes) \
without nested Agent delegation — not for broad read-only exploration (use explore)."
.to_string(),
system_prompt: GENERAL_PURPOSE_SYSTEM_PROMPT.to_string(),
max_turns: 25,
tool_strategy: ToolStrategy::Denylist(vec!["Agent".to_string()]),
}
Expand Down Expand Up @@ -118,6 +129,21 @@ impl Default for AgentRegistry {
mod tests {
use super::*;

#[test]
fn test_explore_system_prompt_covers_search_strategy() {
let def = AgentDefinition::explore();
assert!(def.system_prompt.contains("READ-ONLY"));
assert!(def.system_prompt.contains("broad recursive pattern"));
assert!(def.system_prompt.contains("thoroughness"));
}

#[test]
fn test_plan_system_prompt_is_read_only_architect() {
let def = AgentDefinition::plan();
assert!(def.system_prompt.contains("READ-ONLY"));
assert!(def.system_prompt.contains("Do not execute the plan"));
}

#[test]
fn test_resolve_explore_succeeds() {
let registry = AgentRegistry::builtin();
Expand Down
7 changes: 7 additions & 0 deletions crates/aish-llm/src/agents/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::sync::Arc;
use aish_core::LlmEvent;
use uuid::Uuid;

use crate::prompt::PromptContext;
use crate::session::LlmSession;
use crate::tool_context::ToolExecutionPolicy;
use crate::types::{ChatMessage, LlmCallbackResult, ToolSpec};
Expand All @@ -27,13 +28,15 @@ pub fn effective_max_turns(def_max_turns: u32) -> u32 {
pub struct SpawnConfig {
pub max_turns: u32,
pub system_message: Option<String>,
pub prompt_context: PromptContext,
}

impl Default for SpawnConfig {
fn default() -> Self {
Self {
max_turns: 20,
system_message: None,
prompt_context: PromptContext::MainChat,
}
}
}
Expand Down Expand Up @@ -65,6 +68,7 @@ where
let loop_config = ToolLoopConfig {
max_turns: config.max_turns,
system_message: config.system_message,
prompt_context: config.prompt_context,
..ToolLoopConfig::default()
};
let user_msg = ChatMessage::user(prompt);
Expand Down Expand Up @@ -113,6 +117,9 @@ where
SpawnConfig {
max_turns,
system_message: Some(system_prompt),
prompt_context: PromptContext::SubAgent {
subagent_type: agent_type.clone(),
},
},
|sub| {
sub.set_tool_execution_policy(ToolExecutionPolicy {
Expand Down
14 changes: 9 additions & 5 deletions crates/aish-llm/src/agents/tool_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use aish_core::{LlmEvent, LlmEventType};

use crate::client::LlmResponse;
use crate::prompt::{PromptAssembly, PromptContext};
use crate::session::LlmSession;
use crate::streaming::{extract_message_text, StreamParser};
use crate::types::{ChatMessage, MessageContent};
Expand All @@ -17,6 +18,8 @@ pub struct ToolLoopConfig {
pub max_turns: u32,
/// Optional system prompt prepended to the message list.
pub system_message: Option<String>,
/// Prompt assembly context (MainChat vs SubAgent filtering).
pub prompt_context: PromptContext,
/// Prefix prepended to the final text when max turns is reached.
pub incomplete_prefix: String,
}
Expand All @@ -26,6 +29,7 @@ impl Default for ToolLoopConfig {
Self {
max_turns: 20,
system_message: None,
prompt_context: PromptContext::MainChat,
incomplete_prefix: INCOMPLETE_PREFIX.to_string(),
}
}
Expand Down Expand Up @@ -90,16 +94,16 @@ pub async fn run_tool_loop_until_done(
context_messages: &[ChatMessage],
config: &ToolLoopConfig,
) -> LoopOutcome {
let base_system = config.system_message.as_deref().unwrap_or("");
let bundle = PromptAssembly::build(session, config.prompt_context.clone(), base_system);
let mut messages: Vec<ChatMessage> = Vec::new();
if let Some(sys) = &config.system_message {
messages.push(ChatMessage::system(
session.system_prompt_with_tool_prompts(sys),
));
if config.system_message.is_some() {
messages.push(ChatMessage::system(bundle.system_message));
}
messages.extend_from_slice(context_messages);
messages.push(user_msg.clone());

let tool_specs = session.filtered_tool_specs();
let tool_specs = bundle.tool_specs;
let has_tools = !tool_specs.is_empty();

let mut iterations = 0u32;
Expand Down
2 changes: 2 additions & 0 deletions crates/aish-llm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub mod models;
pub mod oauth;
pub mod openai_sse_bridge;
pub mod probe;
pub mod prompt;
pub mod provider;
pub mod providers;
pub mod session;
Expand Down Expand Up @@ -58,6 +59,7 @@ pub use oauth::{
login_with_device_code, open_url, save_tokens, OAuthProviderSpec, OAuthTokens, PkcePair,
};
pub use probe::probe_live_tool_support;
pub use prompt::{PromptAssembly, PromptBundle, PromptContext, ToolVisibilityPolicy};
pub use provider::{
detect_provider, detect_provider_from_model, refine_provider_from_api_base, ProviderInfo,
};
Expand Down
Loading
Loading