1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! AI 会話ルーティング
//!
//! 自然言語入力を AI に送信し、新規会話または継続会話を処理する。
//! AI の応答(コマンド or 自然言語)に応じて適切なアクションを実行する。
use tracing::{debug, warn};
use crate::ai::AiResponse;
use crate::cli::jarvis::jarvis_notice;
use crate::engine::{execute, CommandResult};
use super::Shell;
/// AI ルーティングの結果
pub(super) struct AiRoutingResult {
/// コマンド実行結果
pub result: CommandResult,
/// AI の Tool Call から発行されたコマンドかどうか
pub from_tool_call: bool,
/// 終了コードを更新すべきかどうか(NaturalLanguage 応答時は false)
pub should_update_exit_code: bool,
/// AI が実際に実行したコマンド文字列(矢印キー履歴に追加するため)
pub executed_command: Option<String>,
}
impl Shell {
/// 自然言語入力を AI にルーティングする。
///
/// 既存の会話コンテキストがある場合は継続会話、なければ新規会話を開始する。
/// AI が無効な場合はコマンドとして直接実行にフォールバックする。
pub(super) async fn route_to_ai(&mut self, line: &str) -> AiRoutingResult {
let ai = match self.ai_client {
Some(ref ai) => ai,
None => {
debug!(ai_enabled = false, "AI disabled, executing directly");
return AiRoutingResult {
result: execute(line),
from_tool_call: false,
should_update_exit_code: true,
executed_command: None,
};
}
};
debug!(ai_enabled = true, "Routing natural language to AI");
// 既存の会話コンテキストがある場合は継続、なければ新規会話
let existing_conv = self.conversation_state.take();
if let Some(mut conv) = existing_conv {
// === 会話継続 ===
debug!(input = %line, "Continuing existing conversation");
match ai.continue_conversation(&mut conv, line).await {
Ok(AiResponse::Command(ref cmd)) => {
debug!(
ai_response = "Command",
command = %cmd,
"AI continued conversation with a command"
);
jarvis_notice(cmd);
let mut result = execute(cmd);
if result.stdout.is_empty() {
result.stdout = format!("[Jarvis executed: {cmd}]");
} else {
result.stdout = format!("[Jarvis executed: {cmd}]\n{}", result.stdout);
}
// 会話コンテキストを維持
self.conversation_state = Some(conv);
AiRoutingResult {
result,
from_tool_call: true,
should_update_exit_code: true,
executed_command: Some(cmd.clone()),
}
}
Ok(AiResponse::NaturalLanguage(ref text)) => {
debug!(
ai_response = "NaturalLanguage",
response_length = text.len(),
"AI continued conversation with natural language"
);
// 会話コンテキストを維持
self.conversation_state = Some(conv);
AiRoutingResult {
result: CommandResult::success(text.clone()),
from_tool_call: false,
// コマンド未実行のため終了コードは更新しない
should_update_exit_code: false,
executed_command: None,
}
}
Err(e) => {
warn!(
error = %e,
input = %line,
"Conversation continuation failed, falling back to direct execution"
);
AiRoutingResult {
result: execute(line),
from_tool_call: false,
should_update_exit_code: true,
executed_command: None,
}
}
}
} else {
// === 新規会話 ===
// BlackBox から直近 5 件のコマンド履歴をコンテキストとして取得
let context = self
.black_box
.as_ref()
.and_then(|bb| bb.get_recent_context(5).ok())
.unwrap_or_default();
debug!(context_length = context.len(), "Context retrieved for AI");
match ai.process_input(line, &context).await {
Ok(conv_result) => match conv_result.response {
AiResponse::Command(ref cmd) => {
debug!(
ai_response = "Command",
command = %cmd,
"AI interpreted natural language as a command"
);
// AI が自然言語からコマンドを解釈 → 実行前にアナウンス
jarvis_notice(cmd);
let mut result = execute(cmd);
// AI が実行したコマンドをコンテキストとして stdout に記録
if result.stdout.is_empty() {
result.stdout = format!("[Jarvis executed: {cmd}]");
} else {
result.stdout = format!("[Jarvis executed: {cmd}]\n{}", result.stdout);
}
AiRoutingResult {
result,
from_tool_call: true,
should_update_exit_code: true,
executed_command: Some(cmd.clone()),
}
}
AiResponse::NaturalLanguage(ref text) => {
debug!(
ai_response = "NaturalLanguage",
response_length = text.len(),
"AI responded with natural language"
);
// 会話コンテキストを保存
self.conversation_state = Some(conv_result.conversation);
AiRoutingResult {
result: CommandResult::success(text.clone()),
from_tool_call: false,
// コマンド未実行のため終了コードは更新しない
should_update_exit_code: false,
executed_command: None,
}
}
},
Err(e) => {
warn!(
error = %e,
input = %line,
"AI processing failed, falling back to direct execution"
);
AiRoutingResult {
result: execute(line),
from_tool_call: false,
should_update_exit_code: true,
executed_command: None,
}
}
}
}
}
}