Complete local-first Rig provider migration

This commit is contained in:
2026-08-06 11:37:28 -05:00
parent f850bae77c
commit 634ce7ba00
38 changed files with 3837 additions and 1616 deletions
+40 -16
View File
@@ -7,7 +7,10 @@ use galaxy_agent_core::{
turn_control, AgentError, AgentEvent, AgentRuntime, MessageContent, MessageRole, ToolCall,
ToolCallDecision, ToolEvent, ToolPolicy, ToolResult, TurnCommand,
};
use galaxy_agent_rig::{OpenAICompatibleRuntime, OpenAICompatibleRuntimeConfig};
use galaxy_agent_rig::{
ChatGPTSubscriptionRuntime, ChatGPTSubscriptionRuntimeConfig, OpenAICompatibleRuntime,
OpenAICompatibleRuntimeConfig,
};
use uuid::Uuid;
use warp_multi_agent_api::ToolType;
@@ -24,6 +27,7 @@ use crate::ai::openai::client::OpenAIClientConfig;
use crate::ai::provider::types::{ContentPart, ConversationMessage};
use crate::ai::runtime::{RuntimeResponseConfig, RuntimeResponseTranslator};
use crate::server::server_api::AIApiError;
use crate::settings::OpenAIProviderKind;
pub(crate) fn rig_openai_response_stream(
config: OpenAIClientConfig,
@@ -35,21 +39,41 @@ pub(crate) fn rig_openai_response_stream(
let skill_path_origin = params.session_context.skill_path_origin();
let prepared = prepare_rig_turn(&config, params, supported_tools, supported_cli_agent_tools);
let model_id = prepared.request.model.as_str().to_string();
let runtime = OpenAICompatibleRuntime::new(OpenAICompatibleRuntimeConfig {
base_url: config.base_url,
api_key: config.api_key,
model: model_id.clone(),
max_output_tokens: config.max_output_tokens.map(u64::from),
supports_system_messages: config.supports_system_messages,
});
rig_response_stream(
runtime,
prepared,
skill_path_origin,
config.max_input_tokens,
"rig_openai_compatible",
cancellation_rx,
)
match config.kind {
OpenAIProviderKind::OpenAICompatible => {
let runtime = OpenAICompatibleRuntime::new(OpenAICompatibleRuntimeConfig {
base_url: config.base_url,
api_key: config.api_key,
model: model_id.clone(),
max_output_tokens: config.max_output_tokens.map(u64::from),
supports_system_messages: config.supports_system_messages,
});
rig_response_stream(
runtime,
prepared,
skill_path_origin,
config.max_input_tokens,
"rig_openai_compatible",
cancellation_rx,
)
}
OpenAIProviderKind::ChatGPTSubscription => {
let runtime = ChatGPTSubscriptionRuntime::new(ChatGPTSubscriptionRuntimeConfig {
model: model_id,
reasoning_effort: config.reasoning_effort,
max_output_tokens: config.max_output_tokens.map(u64::from),
auth_file: None,
});
rig_response_stream(
runtime,
prepared,
skill_path_origin,
config.max_input_tokens,
"rig_chatgpt_subscription",
cancellation_rx,
)
}
}
}
pub(crate) async fn rig_bedrock_response_stream(
+20 -2
View File
@@ -113,7 +113,15 @@ fn prepare_rig_turn_for_provider(
supported_tools
}
};
let (tools, mcp_tool_aliases) = tool_definitions(&available_tools, mcp_context.as_ref());
let (mut tools, mcp_tool_aliases) = tool_definitions(&available_tools, mcp_context.as_ref());
if matches!(mode, RigRequestMode::Cli) {
// History recall cannot advance a running command and is handled inline by the Rig
// adapter (without producing a client action that can trigger another turn). Keeping it
// in the CLI tool list lets the model spend its entire monitor turn recalling the prior
// snapshot instead of scheduling `read_shell_command_output`, so make polling the only
// way to inspect the active command here.
tools.retain(|tool| tool.name != "recall_tool_history");
}
let system_prompt = build_system_prompt(&input, &tools, &global_rules, mode);
let mut new_messages = input_messages(input, tool_results);
@@ -406,6 +414,16 @@ enum RigRequestMode {
fn request_mode(inputs: &[AIAgentInput]) -> RigRequestMode {
for input in inputs {
// A direct-provider follow-up carries an LRC snapshot as an action result rather than
// as a user query with `running_command`. Treat that result as a CLI-monitor turn so the
// request receives the dedicated polling instructions and CLI tool set. Without this,
// the model sees a generic tool-result turn and may stop after inspecting the snapshot
// (or call history recall) instead of scheduling the next output read.
if let AIAgentInput::ActionResult { result, .. } = input {
if result.result.triggers_server_subagent() {
return RigRequestMode::Cli;
}
}
if matches!(
input,
AIAgentInput::UserQuery {
@@ -719,7 +737,7 @@ fn build_system_prompt(
"## Orchestration Mode\nDelegate only independent, bounded work where parallelism materially helps, then synthesize the results.\n\n",
),
RigRequestMode::Cli => prompt.push_str(
"## Running Command Monitor\nMonitor the existing command by its command ID. Never start a duplicate command. Poll briefly, respect stop conditions, and report only verified outcomes.\n\n",
"## Running Command Monitor\nThis turn concerns a running or just-finished shell command. Act as its dedicated monitor while still following the user's steering messages. Use the command ID from the tool result for every read/write operation. If the result says the command finished, report its outcome and stop polling. Otherwise, poll with `read_shell_command_output` and use short delays. Never choose a poll interval that crosses a user-specified deadline or stop condition. When an explicit stop condition is met, call `interrupt_shell_command` immediately, then poll briefly to verify the outcome. Never start a duplicate command merely to check its state, and never report completion while a result says it is still running.\n\n",
),
}
prompt.push_str("## Available Tools\n");
+84 -1
View File
@@ -9,8 +9,10 @@ use warp_multi_agent_api::ToolType;
use super::{input_messages, prepare_bedrock_rig_turn, prepare_rig_turn, tool_definitions};
use crate::ai::agent::api::RequestParams;
use crate::ai::agent::task::TaskId;
use crate::ai::agent::{
AIAgentContext, AIAgentInput, AnyFileContent, FileContext, MCPContext, MCPServer, UserQueryMode,
AIAgentActionId, AIAgentActionResult, AIAgentActionResultType, AIAgentContext, AIAgentInput,
AnyFileContent, FileContext, MCPContext, MCPServer, RequestCommandOutputResult, UserQueryMode,
};
use crate::ai::llms::LLMId;
use crate::ai::openai::client::OpenAIClientConfig;
@@ -18,9 +20,11 @@ use crate::ai::skills::SkillDescriptor;
fn config() -> OpenAIClientConfig {
OpenAIClientConfig {
kind: crate::settings::OpenAIProviderKind::OpenAICompatible,
base_url: "http://localhost:4000/v1".to_string(),
api_key: None,
model: Some("provider-model".to_string()),
reasoning_effort: None,
max_input_tokens: Some(128_000),
max_output_tokens: Some(8_192),
use_rig: true,
@@ -131,6 +135,85 @@ fn rig_prompt_requires_follow_through_without_manual_continue_prompts() {
assert!(prompt.contains("After each tool result, choose and perform the next necessary step"));
}
#[test]
fn lrc_snapshot_follow_up_uses_the_cli_monitor_prompt_and_tools() {
let block_id: galaxy_terminal::model::BlockId = "precmd-lrc-test".to_string().into();
let result = AIAgentActionResult {
id: AIAgentActionId::from("run-call".to_owned()),
task_id: TaskId::new("task".to_owned()),
result: AIAgentActionResultType::RequestCommandOutput(
RequestCommandOutputResult::LongRunningCommandSnapshot {
block_id: block_id.clone(),
command: "bash loop.sh".to_string(),
grid_contents: "Running for 2 seconds...".to_string(),
cursor: String::new(),
is_alt_screen_active: false,
},
),
};
let snapshot_tool_result = ToolResult {
call_id: "run-call".to_string(),
content: result.result.model_content(),
status: ToolResultStatus::Success,
};
let mut params = RequestParams::new_for_test();
params.message_history = vec![galaxy_agent_core::ConversationMessage {
role: MessageRole::Assistant,
content: MessageContent::ToolUse {
tool_use_id: "run-call".to_string(),
name: "run_shell_command".to_string(),
input: serde_json::json!({
"command": "bash loop.sh",
"wait_until_complete": false,
}),
},
}];
params.input = vec![AIAgentInput::ActionResult {
result,
context: Arc::from([]),
}];
params.tool_results = vec![snapshot_tool_result];
let prepared = prepare_rig_turn(
&config(),
params,
vec![ToolType::RunShellCommand],
vec![ToolType::ReadShellCommandOutput],
);
let prompt = prepared.request.system_prompt.expect("system prompt");
assert!(prompt.contains("## Running Command Monitor"));
assert!(prompt.contains("poll with `read_shell_command_output`"));
assert!(prepared
.request
.tools
.iter()
.any(|tool| tool.name == "read_shell_command_output"));
assert!(!prepared
.request
.tools
.iter()
.any(|tool| tool.name == "recall_tool_history"));
assert!(prepared
.request
.messages
.iter()
.any(|message| match &message.content {
MessageContent::ToolResult { content, .. } => {
content.contains("Command ID: precmd-lrc-test")
&& content.contains("Continue monitoring with `read_shell_command_output`")
}
MessageContent::MultiPart(parts) => parts.iter().any(|part| {
matches!(
part,
ContentPart::ToolResult { content, .. }
if content.contains("Command ID: precmd-lrc-test")
)
}),
_ => false,
}));
}
#[test]
fn rig_prompt_requires_matching_project_skills_to_be_read_before_action() {
let skill_path = LocalOrRemotePath::Local(PathBuf::from(