first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,19 +1,29 @@
|
||||
use std::{collections::HashMap, ffi::OsString, path::PathBuf, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use galaxy_cli::agent::Harness;
|
||||
use galaxy_managed_secrets::ManagedSecretValue;
|
||||
use shell_words::quote as shell_quote;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::ai::{
|
||||
agent_sdk::{
|
||||
driver::AgentDriverError, task_env_vars, validate_cli_installed, ClaudeHarness,
|
||||
ThirdPartyHarness,
|
||||
},
|
||||
ambient_agents::{task::HarnessConfig, AgentConfigSnapshot, AmbientAgentTaskId},
|
||||
use crate::ai::agent_sdk::driver::harness::claude_code::prepare_claude_environment_config;
|
||||
use crate::ai::agent_sdk::driver::harness::{
|
||||
harness_kind, harness_model_env_vars, remove_claude_externally_managed_listener_env_vars,
|
||||
HarnessKind,
|
||||
};
|
||||
use crate::ai::agent_sdk::driver::AgentDriverError;
|
||||
use crate::ai::agent_sdk::{task_env_vars, validate_cli_installed};
|
||||
use crate::ai::ambient_agents::task::{
|
||||
normalize_orchestrator_agent_name, HarnessConfig, HarnessModelConfig,
|
||||
};
|
||||
use crate::ai::ambient_agents::{AgentConfigSnapshot, AmbientAgentTaskId};
|
||||
use crate::ai::local_harness_setup::local_harness_product_disabled_message;
|
||||
use crate::server::server_api::ai::AIClient;
|
||||
use crate::terminal::cli_agent_sessions::plugin_manager::plugin_manager_for;
|
||||
use crate::terminal::cli_agent_sessions::plugin_manager::{
|
||||
plugin_manager_for, CliAgentPluginManager,
|
||||
};
|
||||
use crate::terminal::shell::ShellType;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -24,6 +34,38 @@ pub(super) struct PreparedLocalHarnessLaunch {
|
||||
pub task_id: AmbientAgentTaskId,
|
||||
}
|
||||
|
||||
async fn ensure_local_claude_child_plugins(manager: &dyn CliAgentPluginManager) {
|
||||
// Most environments should follow the standard Claude plugin setup path so
|
||||
// hidden local children retain the same notification support as regular
|
||||
// Claude sessions. The exception is local marketplace override testing:
|
||||
// installing/updating the notification plugin re-adds the public
|
||||
// claude-code-warp marketplace, which clobbers a developer's local
|
||||
// claude-code-warp-internal override used for oz-harness-support testing.
|
||||
if !manager.has_local_marketplace_override() {
|
||||
let plugin_result = if manager.needs_update() {
|
||||
manager.update().await
|
||||
} else if !manager.is_installed() {
|
||||
manager.install().await
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
if let Err(error) = plugin_result {
|
||||
log::warn!("Claude notification plugin setup failed for child harness: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
let platform_plugin_result = if manager.platform_plugin_needs_update() {
|
||||
manager.update_platform_plugin().await
|
||||
} else if !manager.is_platform_plugin_installed() {
|
||||
manager.install_platform_plugin().await
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
if let Err(error) = platform_plugin_result {
|
||||
log::warn!("Claude platform plugin setup failed for child harness: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn normalize_local_child_harness(harness_type: &str) -> Option<Harness> {
|
||||
Harness::parse_local_child_harness(harness_type)
|
||||
}
|
||||
@@ -42,6 +84,39 @@ pub(super) fn validate_local_harness_shell(shell_type: Option<ShellType>) -> Res
|
||||
}
|
||||
}
|
||||
|
||||
const LOCAL_CLAUDE_CHILD_ORCHESTRATION_INSTRUCTIONS: &str = r#"You are a local Claude Code child agent launched by a lead agent in Warp.
|
||||
|
||||
Coordinate with the lead agent through the Oz CLI messaging environment:
|
||||
- Your run id is in OZ_RUN_ID.
|
||||
- The lead agent id is in OZ_PARENT_RUN_ID.
|
||||
- The Oz CLI command is in OZ_CLI.
|
||||
|
||||
If OZ_CLI, OZ_RUN_ID, or OZ_PARENT_RUN_ID is missing, report that blocker in your final response.
|
||||
Do not use Claude Code Agent or SendMessage tools to contact the lead agent; use the Oz CLI commands below.
|
||||
Do not ask to inspect help before messaging. The command shapes below are complete.
|
||||
|
||||
Send a message to the lead agent at start, when blocked, and when complete:
|
||||
"$OZ_CLI" run message send --sender-run-id "$OZ_RUN_ID" --to "$OZ_PARENT_RUN_ID" --subject "<subject>" --body "<body>"
|
||||
All four send arguments are required: --sender-run-id "$OZ_RUN_ID", --to "$OZ_PARENT_RUN_ID", --subject, and --body.
|
||||
Do not pass "$OZ_PARENT_RUN_ID" as a positional argument to send.
|
||||
|
||||
After sending a message, and before ending or standing by, check recent inbox messages:
|
||||
"$OZ_CLI" run message list "$OZ_RUN_ID" --limit 25
|
||||
|
||||
The plugin may already have read incoming messages while staging them, so do not rely on --unread.
|
||||
If recent messages from "$OZ_PARENT_RUN_ID" are present and you have not handled them, read them and use the latest lead-agent mailbox message as task context:
|
||||
"$OZ_CLI" run message read "$MESSAGE_ID"
|
||||
|
||||
If a surfaced message requires acknowledgement, mark it delivered:
|
||||
"$OZ_CLI" run message mark-delivered "$MESSAGE_ID"
|
||||
"#;
|
||||
|
||||
pub(super) fn local_claude_child_prompt(task_prompt: &str) -> String {
|
||||
format!(
|
||||
"{LOCAL_CLAUDE_CHILD_ORCHESTRATION_INSTRUCTIONS}\nTask:\n{}",
|
||||
task_prompt
|
||||
)
|
||||
}
|
||||
pub(super) fn build_local_claude_child_command(prompt: &str) -> String {
|
||||
let session_id = Uuid::new_v4();
|
||||
let quoted_prompt = shell_quote(prompt);
|
||||
@@ -56,25 +131,48 @@ pub(super) fn build_local_opencode_child_command(prompt: &str) -> String {
|
||||
let quoted_prompt = shell_quote(prompt);
|
||||
format!("opencode --prompt {quoted_prompt}")
|
||||
}
|
||||
pub(super) fn build_local_codex_child_command(prompt: &str) -> String {
|
||||
let quoted_prompt = shell_quote(prompt);
|
||||
format!("codex --dangerously-bypass-approvals-and-sandbox {quoted_prompt}")
|
||||
}
|
||||
|
||||
fn local_child_task_config(harness: Harness) -> Option<AgentConfigSnapshot> {
|
||||
pub(super) fn local_child_task_config(
|
||||
harness: Harness,
|
||||
agent_name: Option<String>,
|
||||
) -> Option<AgentConfigSnapshot> {
|
||||
let agent_name = agent_name
|
||||
.as_deref()
|
||||
.and_then(normalize_orchestrator_agent_name);
|
||||
match harness {
|
||||
Harness::Oz | Harness::OpenCode | Harness::Gemini | Harness::Unknown => None,
|
||||
Harness::Claude => Some(AgentConfigSnapshot {
|
||||
harness: Some(HarnessConfig::from_harness_type(harness)),
|
||||
..Default::default()
|
||||
}),
|
||||
Harness::Oz | Harness::Unknown => None,
|
||||
Harness::Claude | Harness::OpenCode | Harness::Gemini | Harness::Codex => {
|
||||
Some(AgentConfigSnapshot {
|
||||
name: agent_name,
|
||||
harness: Some(HarnessConfig::from_harness_type(harness)),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn prepare_local_harness_child_launch(
|
||||
prompt: String,
|
||||
harness_type: String,
|
||||
model_id: Option<String>,
|
||||
parent_run_id: Option<String>,
|
||||
agent_name: Option<String>,
|
||||
shell_type: Option<ShellType>,
|
||||
startup_directory: Option<PathBuf>,
|
||||
ai_client: Arc<dyn AIClient>,
|
||||
) -> Result<PreparedLocalHarnessLaunch, String> {
|
||||
let harness_model_config =
|
||||
model_id
|
||||
.filter(|id| !id.is_empty())
|
||||
.map(|model_id| HarnessModelConfig {
|
||||
model_id,
|
||||
reasoning_level: None,
|
||||
});
|
||||
let Some(harness) = normalize_local_child_harness(&harness_type) else {
|
||||
let harness_name = harness_type.trim();
|
||||
return Err(if harness_name.is_empty() {
|
||||
@@ -83,6 +181,9 @@ pub(super) async fn prepare_local_harness_child_launch(
|
||||
format!("Unsupported local child harness '{harness_name}'.")
|
||||
});
|
||||
};
|
||||
if let Some(message) = local_harness_product_disabled_message(harness) {
|
||||
return Err(message.to_string());
|
||||
}
|
||||
validate_local_harness_shell(shell_type)?;
|
||||
let command = match harness {
|
||||
Harness::Oz => unreachable!("normalize_local_child_harness filters out Oz"),
|
||||
@@ -91,32 +192,46 @@ pub(super) async fn prepare_local_harness_child_launch(
|
||||
let working_dir = startup_directory
|
||||
.or_else(|| std::env::current_dir().ok())
|
||||
.ok_or_else(|| {
|
||||
"Could not resolve a working directory for the local Claude child.".to_string()
|
||||
format!(
|
||||
"Could not resolve a working directory for the local {} child.",
|
||||
harness.display_name()
|
||||
)
|
||||
})?;
|
||||
let claude_harness = ClaudeHarness;
|
||||
claude_harness
|
||||
let HarnessKind::ThirdParty(third_party_harness) =
|
||||
harness_kind(harness).map_err(|error: AgentDriverError| error.to_string())?
|
||||
else {
|
||||
unreachable!("Claude resolves to a third-party harness")
|
||||
};
|
||||
third_party_harness
|
||||
.validate()
|
||||
.map_err(|error: AgentDriverError| error.to_string())?;
|
||||
// Local child harness panes inherit the user's existing local Claude
|
||||
// auth/session state. We still prepare Claude's config files here,
|
||||
// Local child harness panes inherit the user's existing local
|
||||
// auth/session state. We still prepare harness config files here,
|
||||
// but there are no Warp-managed secrets to materialize into the
|
||||
// hidden child pane.
|
||||
let managed_secrets: HashMap<String, ManagedSecretValue> = HashMap::new();
|
||||
claude_harness
|
||||
.prepare_environment_config(&working_dir, None, &managed_secrets)
|
||||
.map_err(|error: AgentDriverError| error.to_string())?;
|
||||
if let Some(manager) = plugin_manager_for(claude_harness.cli_agent()) {
|
||||
if let Err(error) = manager.install().await {
|
||||
log::warn!("Claude plugin installation failed for child harness: {error}");
|
||||
}
|
||||
if let Err(error) = manager.install_platform_plugin().await {
|
||||
log::warn!(
|
||||
"Claude platform plugin installation failed for child harness: {error}"
|
||||
);
|
||||
}
|
||||
prepare_claude_environment_config(&working_dir, &HashMap::new())
|
||||
.map_err(|error| error.to_string())?;
|
||||
if let Some(manager) = plugin_manager_for(third_party_harness.cli_agent()) {
|
||||
ensure_local_claude_child_plugins(manager.as_ref()).await;
|
||||
}
|
||||
|
||||
build_local_claude_child_command(&prompt)
|
||||
build_local_claude_child_command(&local_claude_child_prompt(&prompt))
|
||||
}
|
||||
Harness::Codex => {
|
||||
let HarnessKind::ThirdParty(third_party_harness) =
|
||||
harness_kind(harness).map_err(|error: AgentDriverError| error.to_string())?
|
||||
else {
|
||||
unreachable!("Codex resolves to a third-party harness")
|
||||
};
|
||||
third_party_harness
|
||||
.validate()
|
||||
.map_err(|error: AgentDriverError| error.to_string())?;
|
||||
|
||||
// Local Codex child panes must rely on the user's existing local
|
||||
// auth/session state. Do not run the shared Codex environment prep
|
||||
// here: it can seed OPENAI_API_KEY into ~/.codex/auth.json and
|
||||
// rewrite ~/.codex/config.toml for the whole machine.
|
||||
build_local_codex_child_command(&prompt)
|
||||
}
|
||||
Harness::OpenCode => {
|
||||
validate_cli_installed("opencode", Some("https://opencode.ai/docs"))
|
||||
@@ -131,7 +246,7 @@ pub(super) async fn prepare_local_harness_child_launch(
|
||||
prompt.clone(),
|
||||
None,
|
||||
parent_run_id.clone(),
|
||||
local_child_task_config(harness),
|
||||
local_child_task_config(harness, agent_name),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
@@ -141,9 +256,25 @@ pub(super) async fn prepare_local_harness_child_launch(
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut env_vars = task_env_vars(Some(&task_id), parent_run_id.as_deref(), harness);
|
||||
if harness == Harness::Claude {
|
||||
// Local Claude child panes are launched directly in hidden terminals,
|
||||
// not through AgentDriver's ClaudeHarnessRunner. Let the Claude plugin
|
||||
// manage its own listener instead of waiting for a non-existent
|
||||
// external MessageBridge.
|
||||
remove_claude_externally_managed_listener_env_vars(&mut env_vars);
|
||||
}
|
||||
// Propagate the selected model to Claude Code via ANTHROPIC_MODEL.
|
||||
// Codex local children never receive a model override — the UI
|
||||
// ensures model_id is empty for local Codex.
|
||||
env_vars.extend(harness_model_env_vars(
|
||||
harness,
|
||||
harness_model_config.as_ref(),
|
||||
));
|
||||
|
||||
Ok(PreparedLocalHarnessLaunch {
|
||||
command,
|
||||
env_vars: task_env_vars(Some(&task_id), parent_run_id.as_deref(), harness),
|
||||
env_vars,
|
||||
run_id: task_id.to_string(),
|
||||
task_id,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user