Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ aish run --config ~/work/ai-shell-config.yaml
| `context_token_budget` | integer/null | `null` | 可选的上下文 token 预算限制 | 如:4000,为 null 则仅使用消息数量限制 |
| `enable_token_estimation` | boolean | `true` | 启用基于 tiktoken 的 token 估算 | true/false |
| `context_auto_compact` | object | 见下方 | 自动上下文压缩配置,面向 shell/tool 输出控量 | 可选 |
| `task_budget` | object | 见下方 | 任务级累计预算(issue #569),任一维度达到上限即停止循环并提示(继续不会重置;调整预算需显式确认并写入审计) | 各维度 integer/null |

### 工具输出配置

Expand Down Expand Up @@ -203,6 +204,14 @@ max_llm_messages: 50
max_shell_messages: 20
context_token_budget: null
enable_token_estimation: true

# 任务级累计预算(可选;缺省不限,仅计数)
task_budget:
max_rounds: null # 累计工具循环轮次
max_tool_calls: null # 累计工具调用次数
max_tokens: null # 累计 token(输入+输出,不含缓存命中)
max_duration_secs: null # 累计活跃时长(秒)

context_auto_compact:
enabled: true
full_compact_enabled: true
Expand Down
2 changes: 1 addition & 1 deletion crates/aish-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ pub mod model;
pub use loader::ConfigLoader;
pub use model::{
compile_remote_danger_patterns, ApiAccountConfig, ConfigModel, InlineCompletionConfig,
MemoryConfig, OutputOffloadConfig, RegistrySource, SkillsConfig, ToolArgPreviewConfig,
MemoryConfig, OutputOffloadConfig, RegistrySource, SkillsConfig, TaskBudgetConfig,
};
60 changes: 60 additions & 0 deletions crates/aish-config/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,37 @@ impl Default for ContextAutoCompactConfig {
}
}

// ---------------------------------------------------------------------------
// Task budget sub-config (issue #569)
// ---------------------------------------------------------------------------

/// Task-level cumulative budget bounds. All dimensions are optional and
/// default to unlimited; the runtime still accrues counters for display.
/// A tripped budget stops the loop with `BudgetExhausted`; raising a limit
/// is an explicit, audit-logged action — never implied by "continue".
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(default)]
pub struct TaskBudgetConfig {
/// Cumulative tool-loop rounds for the whole task.
pub max_rounds: Option<u64>,
/// Cumulative tool executions (parallel calls each count).
pub max_tool_calls: Option<u64>,
/// Cumulative fresh tokens (input + output; cached reads excluded).
pub max_tokens: Option<u64>,
/// Cumulative active wall-clock seconds (turn time only, no idle).
pub max_duration_secs: Option<u64>,
}

impl TaskBudgetConfig {
/// True when any dimension is bounded.
pub fn is_bounded(&self) -> bool {
self.max_rounds.is_some()
|| self.max_tool_calls.is_some()
|| self.max_tokens.is_some()
|| self.max_duration_secs.is_some()
}
}

// ---------------------------------------------------------------------------
// Inline AI completion sub-config
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -483,6 +514,11 @@ pub struct ConfigModel {
#[serde(default)]
pub context_token_budget: Option<usize>,

/// Task-level cumulative budget (issue #569). Every dimension is
/// optional; absent = unlimited (counters still accrue for /token).
#[serde(default)]
pub task_budget: TaskBudgetConfig,

/// Enable tiktoken-based token estimation for context trimming
#[serde(default = "default_true")]
pub enable_token_estimation: bool,
Expand Down Expand Up @@ -582,6 +618,7 @@ impl Default for ConfigModel {
context_token_budget: None,
enable_token_estimation: default_true(),
context_auto_compact: ContextAutoCompactConfig::default(),
task_budget: TaskBudgetConfig::default(),
enable_scripts: default_true(),
history_size: default_history_size(),
terminal_resize_mode: default_terminal_resize_mode(),
Expand Down Expand Up @@ -1018,4 +1055,27 @@ inline_completion:
assert_eq!(m.inline_completion.debounce_ms, 400);
assert_eq!(m.inline_completion.max_tokens, 512);
}

#[test]
fn task_budget_config_parses_from_yaml() {
let yaml = r#"
max_llm_messages: 30
task_budget:
max_rounds: 200
max_tokens: 2000000
"#;
let config: ConfigModel = serde_yaml::from_str(yaml).unwrap();
assert!(config.task_budget.is_bounded());
assert_eq!(config.task_budget.max_rounds, Some(200));
assert_eq!(config.task_budget.max_tokens, Some(2_000_000));
assert_eq!(config.task_budget.max_tool_calls, None);
assert_eq!(config.task_budget.max_duration_secs, None);
}

#[test]
fn task_budget_config_defaults_to_unbounded() {
let config = ConfigModel::default();
assert!(!config.task_budget.is_bounded());
assert_eq!(config.task_budget.max_rounds, None);
}
}
6 changes: 6 additions & 0 deletions crates/aish-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ pub enum AishError {
#[error("tool loop stopped at the iteration limit")]
IterationLimit,

/// Task-level cumulative budget exhausted (issue #569). The field names
/// the dimension that tripped (rounds / tool_calls / tokens / duration).
#[error("task budget exhausted: {0}")]
BudgetExhausted(String),

#[error("operation timed out")]
Timeout,
}
Expand Down Expand Up @@ -74,6 +79,7 @@ impl AishError {
AishError::Shell(_) => "shell",
AishError::Cancelled => "cancelled",
AishError::IterationLimit => "iteration_limit",
AishError::BudgetExhausted(_) => "budget_exhausted",
AishError::Parse(_) => "parse",
AishError::Timeout => "timeout",
}
Expand Down
20 changes: 20 additions & 0 deletions crates/aish-i18n/locales/de-DE.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,15 @@ shell:
no_evidence: "(nicht angegeben)"
nl_detection:
confirm_ask_ai: "Das sieht nach einer natürlichen Sprachfrage aus. KI fragen? (Y/n)"
budget:
exhausted_title: "⛔ Aufgabenbudget erschoepft"
exhausted_detail: "Dimension: {dimension} · Genutzt: {used} · Limit: {limit}"
exhausted_options: "Budget anpassen und fortfahren? Anpassungen werden auditiert (y=anpassen / n=stoppen)"
stopped_by_user: "Durch Budget gestoppt. Abgeschlossene Werkzeugschritte sind gespeichert; fortsetzen vom gespeicherten Punkt."
new_limit_prompt: "Neues Limit fuer diese Dimension eingeben:"
raised: "Budget angepasst: {dimension} -> {value}. Aufgabe erneut ausgeben, um fortzufahren."
invalid_limit: "Ungueltiger Wert (muss ueber der Nutzung liegen); Budget unveraendert."

error:
load_script_failed: "Skript '{script}' konnte nicht geladen werden: {error}"
pty_error: "PTY-Fehler: {error}"
Expand All @@ -343,6 +352,7 @@ shell:
llm_error_message: "Fehler: {error}"
partial_turn_saved: "Die abgeschlossenen Werkzeugschritte dieses Durchgangs wurden gespeichert ({steps} Schritte). Die Arbeit vor dem Fehler geht nicht verloren; geben Sie eine neue Aufgabe ein oder ;retry, um ab dem gespeicherten Punkt fortzufahren."
partial_turn_saved_limit: "Die abgeschlossenen Werkzeugschritte dieses Durchgangs wurden vor dem Stopp am Iterationslimit gespeichert ({steps} Schritte). Die Arbeit bleibt erhalten; geben Sie eine neue Aufgabe ein oder ;retry, um ab dem gespeicherten Punkt fortzufahren."
partial_turn_saved_budget: "Die abgeschlossenen Werkzeugschritte dieses Durchgangs wurden vor dem Stopp am Aufgabenbudgetlimit gespeichert ({steps} Schritte). Die Arbeit bleibt erhalten; geben Sie eine neue Aufgabe ein oder ;retry, um vom gespeicherten Punkt fortzufahren."
llm_error_details_title: "Debug-Details"
tool_args_json_invalid_hint: "❌ Anfrage fehlgeschlagen, bitte erneut versuchen."
prompt:
Expand Down Expand Up @@ -446,6 +456,13 @@ shell:
output_tokens: "Ausgabe-Tokens"
total: "Gesamt"
api_calls: "API-Aufrufe"
budget_title: "Aufgabenbudget"
budget_rounds: "Runden (genutzt/Limit):"
budget_tool_calls: "Werkzeugaufrufe (genutzt/Limit):"
budget_tokens: "Token (genutzt/Limit):"
budget_duration: "Aktive Sekunden (genutzt/Limit):"
budget_cost: "Kosten:"
budget_cost_unavailable: "nicht verfuegbar (Anbieter melden keine Preise)"
setup:
no_config_manager: "Neukonfiguration nicht möglich: Konfigurationsmanager nicht verfügbar"
cancelled: "Einrichtung abgebrochen; aktuelle Einstellungen werden beibehalten"
Expand Down Expand Up @@ -559,6 +576,9 @@ shell:
inline_enforce_json:
label: "Inline Enforce JSON"
desc: "Inline-Completion zwingen, striktes JSON zurückzugeben"
task_budget_max_rounds:
label: "Aufgabenbudget-Rundenlimit"
desc: "Kumulierte Werkzeugschleifen-Runden pro Aufgabe; leer = unbegrenzt (Weitermachen setzt nicht zurueck)"
max_llm_messages:
label: "Max LLM-Messages"
desc: "Maximale Konversations-Turns im LLM-Verlauf"
Expand Down
20 changes: 20 additions & 0 deletions crates/aish-i18n/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,15 @@ shell:
nl_detection:
confirm_ask_ai: "This looks like a natural language question. Ask AI? (Y/n)"

budget:
exhausted_title: "⛔ Task budget exhausted"
exhausted_detail: "Dimension: {dimension} · Used: {used} · Limit: {limit}"
exhausted_options: "Adjust the budget and continue? Adjustments are audit-logged (y=adjust / n=stop)"
stopped_by_user: "Stopped by budget. Completed tool evidence is saved; resume from the saved point later."
new_limit_prompt: "Enter the new limit for this dimension:"
raised: "Budget adjusted: {dimension} -> {value}. Re-issue the task to continue from the saved point."
invalid_limit: "Invalid value (must exceed usage); budget unchanged."

error:
execution_error: "❌ Error during execution: {error}"
pty_decode_error: "Caught PTY output decode error: {error}"
Expand All @@ -779,6 +788,7 @@ shell:
llm_error_message: "Error: {error}"
partial_turn_saved: "Saved the completed tool steps of this turn ({steps} steps). Work done before the failure is not lost; enter a new task or ;retry to continue from the saved point."
partial_turn_saved_limit: "Saved the completed tool steps of this turn ({steps} steps) before stopping at the iteration limit. The work is kept; enter a new task or ;retry to continue from the saved point."
partial_turn_saved_budget: "Saved the completed tool steps of this turn ({steps} steps) before stopping at the task budget limit. The work is kept; enter a new task or ;retry to continue from the saved point."
llm_error_details_title: "Debug Details"
tool_args_json_invalid_hint: "❌ Request failed, please retry."

Expand Down Expand Up @@ -888,6 +898,13 @@ shell:
output_tokens: "Output tokens"
total: "Total"
api_calls: "API calls"
budget_title: "Task budget"
budget_rounds: "Rounds (used/limit):"
budget_tool_calls: "Tool calls (used/limit):"
budget_tokens: "Tokens (used/limit):"
budget_duration: "Active seconds (used/limit):"
budget_cost: "Cost:"
budget_cost_unavailable: "unavailable (providers do not report pricing)"

setup:
no_config_manager: "Cannot reconfigure: configuration manager unavailable"
Expand Down Expand Up @@ -983,6 +1000,9 @@ shell:
inline_completion:
label: "Inline AI Completion"
desc: "Gray ghost-text AI suggestions while typing an AI prompt"
task_budget_max_rounds:
label: "Task Budget Round Limit"
desc: "Cumulative tool-loop rounds per task; blank = unlimited (continues never reset it)"
max_llm_messages:
label: "Max AI Messages"
desc: "How many AI conversation turns to keep in context"
Expand Down
20 changes: 20 additions & 0 deletions crates/aish-i18n/locales/es-ES.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,15 @@ shell:
no_evidence: "(no proporcionada)"
nl_detection:
confirm_ask_ai: "Esto parece una pregunta en lenguaje natural. ¿Preguntar a IA? (Y/n)"
budget:
exhausted_title: "⛔ Presupuesto de tarea agotado"
exhausted_detail: "Dimensión: {dimension} · Usado: {used} · Límite: {limit}"
exhausted_options: "¿Ajustar el presupuesto y continuar? Los ajustes se registran en la auditoría (y=ajustar / n=detener)"
stopped_by_user: "Detenido por presupuesto. La evidencia de herramientas completadas está guardada; reanude desde el punto guardado."
new_limit_prompt: "Introduzca el nuevo límite para esta dimensión:"
raised: "Presupuesto ajustado: {dimension} -> {value}. Emita la tarea de nuevo para continuar."
invalid_limit: "Valor inválido (debe superar el uso); presupuesto sin cambios."

error:
load_script_failed: "No se pudo cargar el script '{script}': {error}"
pty_error: "Error de PTY: {error}"
Expand All @@ -343,6 +352,7 @@ shell:
llm_error_message: "Error: {error}"
partial_turn_saved: "Se guardaron los pasos de herramienta completados de este turno ({steps} pasos). El trabajo previo al error no se pierde; escriba una nueva tarea o ;retry para continuar desde el punto guardado."
partial_turn_saved_limit: "Se guardaron los pasos de herramienta completados de este turno ({steps} pasos) antes de detenerse en el límite de iteraciones. El trabajo se conserva; escriba una nueva tarea o ;retry para continuar desde el punto guardado."
partial_turn_saved_budget: "Se guardaron los pasos de herramientas completados de este turno ({steps} pasos) antes de detenerse en el límite de presupuesto de la tarea. El trabajo se conserva; escriba una nueva tarea o ;retry para continuar desde el punto guardado."
llm_error_details_title: "Detalles de depuración"
tool_args_json_invalid_hint: "❌ La solicitud falló; inténtalo de nuevo."
prompt:
Expand Down Expand Up @@ -446,6 +456,13 @@ shell:
output_tokens: "Tokens de salida"
total: "Total"
api_calls: "Llamadas API"
budget_title: "Presupuesto de tarea"
budget_rounds: "Rondas (usado/límite):"
budget_tool_calls: "Llamadas de herramientas (usado/límite):"
budget_tokens: "Tokens (usado/límite):"
budget_duration: "Segundos activos (usado/límite):"
budget_cost: "Costo:"
budget_cost_unavailable: "no disponible (los proveedores no informan precios)"
setup:
no_config_manager: "No se puede reconfigurar: el gestor de configuración no está disponible"
cancelled: "Configuración cancelada; se conservan los ajustes actuales"
Expand Down Expand Up @@ -559,6 +576,9 @@ shell:
inline_enforce_json:
label: "Forzar JSON en línea"
desc: "Forzar que el completado en línea devuelva JSON estricto"
task_budget_max_rounds:
label: "Límite de rondas del presupuesto"
desc: "Rondas acumuladas del bucle de herramientas por tarea; vacío = ilimitado (continuar nunca lo restablece)"
max_llm_messages:
label: "Mensajes LLM máx."
desc: "Máximo de turnos de conversación conservados en el historial del LLM"
Expand Down
20 changes: 20 additions & 0 deletions crates/aish-i18n/locales/fr-FR.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,15 @@ shell:
no_evidence: "(non fournie)"
nl_detection:
confirm_ask_ai: "Cela ressemble à une question en langage naturel. Demander à l'IA ? (Y/n)"
budget:
exhausted_title: "⛔ Budget de tache epuise"
exhausted_detail: "Dimension : {dimension} · Utilise : {used} · Limite : {limit}"
exhausted_options: "Ajuster le budget et continuer ? Les ajustements sont journalises (y=ajuster / n=arreter)"
stopped_by_user: "Arrete par budget. Les preuves d'outils completes sont sauvegardees ; reprenez depuis le point sauvegarde."
new_limit_prompt: "Saisissez la nouvelle limite pour cette dimension :"
raised: "Budget ajuste : {dimension} -> {value}. Relancez la tache pour continuer."
invalid_limit: "Valeur invalide (doit depasser l'utilisation) ; budget inchange."

error:
load_script_failed: "Échec du chargement du script '{script}' : {error}"
pty_error: "Erreur PTY : {error}"
Expand All @@ -343,6 +352,7 @@ shell:
llm_error_message: "Erreur : {error}"
partial_turn_saved: "Les etapes d'outils completees de ce tour ont ete sauvegardees ({steps} etapes). Le travail precedent l'erreur n'est pas perdu ; saisissez une nouvelle tache ou ;retry pour reprendre depuis le point sauvegarde."
partial_turn_saved_limit: "Les etapes d'outils completees de ce tour ont ete sauvegardees ({steps} etapes) avant l'arret a la limite d'iterations. Le travail est conserve ; saisissez une nouvelle tache ou ;retry pour reprendre depuis le point sauvegarde."
partial_turn_saved_budget: "Les etapes d'outils completees de ce tour ont ete sauvegardees ({steps} etapes) avant l'arret a la limite de budget de la tache. Le travail est conserve ; saisissez une nouvelle tache ou ;retry pour continuer depuis le point sauvegarde."
llm_error_details_title: "Details de debogage"
tool_args_json_invalid_hint: "❌ La requete a echoue, veuillez reessayer."
prompt:
Expand Down Expand Up @@ -446,6 +456,13 @@ shell:
output_tokens: "Tokens de sortie"
total: "Total"
api_calls: "Appels API"
budget_title: "Budget de tache"
budget_rounds: "Tours (utilise/limite):"
budget_tool_calls: "Appels d'outils (utilise/limite):"
budget_tokens: "Tokens (utilise/limite):"
budget_duration: "Secondes actives (utilise/limite):"
budget_cost: "Cout:"
budget_cost_unavailable: "indisponible (les fournisseurs ne rapportent pas de prix)"
setup:
no_config_manager: "Reconfiguration impossible : gestionnaire de configuration indisponible"
cancelled: "Configuration annulee, les parametres actuels sont conserves"
Expand Down Expand Up @@ -559,6 +576,9 @@ shell:
inline_enforce_json:
label: "Forcer JSON en ligne"
desc: "Forcer la complétion en ligne à renvoyer du JSON strict"
task_budget_max_rounds:
label: "Limite de tours du budget"
desc: "Tours cumules de la boucle d'outils par tache ; vide = illimite (continuer ne reinitialise jamais)"
max_llm_messages:
label: "Messages LLM max"
desc: "Tours de conversation maximum conservés dans l'historique LLM"
Expand Down
Loading
Loading