Synced from monorepo
Synced from monorepo Changes: - Refresh tool search when the managed MCP catalog is re-fetched - Prevent duplicate leader process spawn and startup hang from stale leaders - Document marketplaces, plugins, and organization controls - Stamp session ID on image generation direct-to-API requests - Fix auto mode blocked documentation - Auto mode considers recent user intent - Expose deploy archive, taken-down, limit, and in-progress reasons on the chat API - Fail-closed auth refresh contract for shell clients - Emit a chat-supplied per-session turn index in turn hooks - Show bash mode chrome in minimal mode - Add metrics for true-noop and stationarity stops - Include voice interim text on prompt submit - Silently end turn on true-noop thrash - Quiet copy toast when clipboard delivery is confirmed - Fix session fork truncating at the wrong prompt in rewound sessions - Make the idle "still running" watcher cue clickable to open the tasks pane - Default web search model to grok-4.5 - Let plugin subagents inherit parent MCP servers - Gate no-op end-turn reminder on system reminders - Add gateway bridge lifecycle telemetry - Allow editing finalized text while voice is open - Relocate token carrier to turn-commit events and plumb per-turn origin context - Raise workflow scratch quotas and make failed runs resumable - Workflows overlay: auto-progress phases, live agent status, and drop budget meter Source-Revision: 9b8d35b46d959c042ea9aa31cbbebbd1f0c5c527
This commit is contained in:
parent
69f0ba880a
commit
6e38642082
103 changed files with 4964 additions and 1261 deletions
|
|
@ -456,7 +456,11 @@ fn is_pure_status_print(trimmed: &str) -> bool {
|
|||
/// - Normal: `exit: N [annotations]\n<stripped_output>`
|
||||
/// - Killed by harness/signal: `exit: killed (reason) [annotations]\n<stripped_output>`
|
||||
/// - Backgrounded: verbose `[Command moved to background]...` format.
|
||||
pub(crate) fn format_default_prompt(bash: &BashOutput) -> String {
|
||||
///
|
||||
/// `append_noop_reminder` gates the no-op-command end-turn `<system-reminder>`.
|
||||
/// Callers pass the session's `SystemRemindersEnabled` value so the nudge
|
||||
/// follows the same switch as every other system reminder.
|
||||
pub(crate) fn format_default_prompt(bash: &BashOutput, append_noop_reminder: bool) -> String {
|
||||
let output_str = if bash.output_for_prompt.is_empty() {
|
||||
let raw = String::from_utf8_lossy(&bash.output);
|
||||
strip_ansi_escapes::strip_str(&raw).to_string()
|
||||
|
|
@ -487,7 +491,7 @@ pub(crate) fn format_default_prompt(bash: &BashOutput) -> String {
|
|||
None => format!("exit: {}{}", bash.exit_code, annotations(bash)),
|
||||
};
|
||||
let prompt = format!("{}\n{}", header, output_str);
|
||||
if bash.signal.is_none() && is_noop_command(&bash.command) {
|
||||
if append_noop_reminder && bash.signal.is_none() && is_noop_command(&bash.command) {
|
||||
format!("{}\n\n{}", prompt.trim_end(), NOOP_END_TURN_REMINDER)
|
||||
} else {
|
||||
prompt
|
||||
|
|
@ -2242,7 +2246,16 @@ impl xai_tool_runtime::Tool for BashTool {
|
|||
output_delta: None,
|
||||
was_bare_echo: false,
|
||||
};
|
||||
bash.output_for_prompt = format_default_prompt(&bash);
|
||||
// Gate the no-op end-turn reminder on the same switch as every other
|
||||
// system reminder (absent resource => enabled, mirroring
|
||||
// `finalize_output`), so toolsets with `system_reminders_enabled=false`
|
||||
// don't receive it.
|
||||
let append_noop_reminder = resources
|
||||
.lock()
|
||||
.await
|
||||
.get::<crate::types::resources::SystemRemindersEnabled>()
|
||||
.is_none_or(|e| e.0);
|
||||
bash.output_for_prompt = format_default_prompt(&bash, append_noop_reminder);
|
||||
|
||||
// Bare `echo "<msg>"` usage (common model anti-pattern for "just output something").
|
||||
// We tag it for statistics (grok_build backend) and can surface an educational
|
||||
|
|
@ -3416,7 +3429,7 @@ mod tests {
|
|||
output_delta: None,
|
||||
was_bare_echo: false,
|
||||
};
|
||||
bash.output_for_prompt = format_default_prompt(&bash);
|
||||
bash.output_for_prompt = format_default_prompt(&bash, /* append_noop_reminder */ true);
|
||||
bash
|
||||
}
|
||||
|
||||
|
|
@ -3469,7 +3482,7 @@ mod tests {
|
|||
let mut bash = make_bash_output(-1, "partial\n");
|
||||
bash.signal = Some("timeout".to_string());
|
||||
bash.timed_out = true;
|
||||
bash.output_for_prompt = format_default_prompt(&bash);
|
||||
bash.output_for_prompt = format_default_prompt(&bash, /* append_noop_reminder */ true);
|
||||
// Synthetic kill reasons render as `exit: killed (reason)` — no
|
||||
// redundant `[signal=…]` / `[timeout]` annotation.
|
||||
assert!(
|
||||
|
|
@ -3514,7 +3527,8 @@ mod tests {
|
|||
for reason in ["timeout", "max_runtime", "cancelled", "killed", "signal 15"] {
|
||||
let mut bash = make_bash_output(-1, "partial\n");
|
||||
bash.signal = Some(reason.to_string());
|
||||
bash.output_for_prompt = format_default_prompt(&bash);
|
||||
bash.output_for_prompt =
|
||||
format_default_prompt(&bash, /* append_noop_reminder */ true);
|
||||
let expected = format!("exit: killed ({})", reason);
|
||||
assert!(
|
||||
bash.output_for_prompt.starts_with(&expected),
|
||||
|
|
@ -3534,7 +3548,7 @@ mod tests {
|
|||
|
||||
let mut oom = make_bash_output(137, "killed\n");
|
||||
oom.signal = Some("oom".to_string());
|
||||
oom.output_for_prompt = format_default_prompt(&oom);
|
||||
oom.output_for_prompt = format_default_prompt(&oom, /* append_noop_reminder */ true);
|
||||
assert!(oom.output_for_prompt.starts_with("exit: 137 [signal=oom]"));
|
||||
}
|
||||
|
||||
|
|
@ -3544,7 +3558,7 @@ mod tests {
|
|||
bash.signal = Some("backgrounded".to_string());
|
||||
bash.output_file = "/tmp/bg.log".to_string();
|
||||
bash.total_bytes = 10000;
|
||||
bash.output_for_prompt = format_default_prompt(&bash);
|
||||
bash.output_for_prompt = format_default_prompt(&bash, /* append_noop_reminder */ true);
|
||||
assert!(
|
||||
bash.output_for_prompt
|
||||
.starts_with("[Command moved to background]")
|
||||
|
|
@ -3587,7 +3601,10 @@ mod tests {
|
|||
"printf hi",
|
||||
"printf 'done\\n'",
|
||||
] {
|
||||
let prompt = format_default_prompt(&bash_output_with_command(cmd, ""));
|
||||
let prompt = format_default_prompt(
|
||||
&bash_output_with_command(cmd, ""),
|
||||
/* append_noop_reminder */ true,
|
||||
);
|
||||
assert!(
|
||||
prompt.contains(NOOP_END_TURN_REMINDER),
|
||||
"no-op command {cmd:?} should append the end-turn reminder, got: {prompt:?}"
|
||||
|
|
@ -3595,6 +3612,23 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// With `append_noop_reminder = false` (session `system_reminders_enabled=false`),
|
||||
/// the no-op end-turn reminder is suppressed even for no-op commands. Mirrors
|
||||
/// gating the reminder on the shared `SystemRemindersEnabled` switch.
|
||||
#[test]
|
||||
fn default_prompt_noop_reminder_suppressed_when_disabled() {
|
||||
for cmd in ["true", ":", "", "echo ok", "printf hi"] {
|
||||
let prompt = format_default_prompt(
|
||||
&bash_output_with_command(cmd, ""),
|
||||
/* append_noop_reminder */ false,
|
||||
);
|
||||
assert!(
|
||||
!prompt.contains("<system-reminder>"),
|
||||
"no-op command {cmd:?} must not append the reminder when disabled, got: {prompt:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_prompt_normal_command_has_no_end_turn_reminder() {
|
||||
for cmd in [
|
||||
|
|
@ -3609,7 +3643,10 @@ mod tests {
|
|||
"echo hi; ls",
|
||||
"printf '%s' \"$x\"",
|
||||
] {
|
||||
let prompt = format_default_prompt(&bash_output_with_command(cmd, "hi\n"));
|
||||
let prompt = format_default_prompt(
|
||||
&bash_output_with_command(cmd, "hi\n"),
|
||||
/* append_noop_reminder */ true,
|
||||
);
|
||||
assert!(
|
||||
!prompt.contains("<system-reminder>"),
|
||||
"normal command {cmd:?} must not append the end-turn reminder, got: {prompt:?}"
|
||||
|
|
|
|||
|
|
@ -299,12 +299,26 @@ pub enum ImageGenConfig {
|
|||
},
|
||||
}
|
||||
|
||||
/// Session-id header attached to imagine API requests; matches the header
|
||||
/// chat requests already carry.
|
||||
pub const SESSION_ID_HEADER: &str = "x-grok-session-id";
|
||||
|
||||
impl ImageGenConfig {
|
||||
/// Credentials present — required to construct any of the clients.
|
||||
pub fn has_credentials(&self) -> bool {
|
||||
matches!(self, Self::Enabled { .. })
|
||||
}
|
||||
|
||||
/// Stamp [`SESSION_ID_HEADER`] onto `extra_headers`. A caller-provided
|
||||
/// value is never overwritten. No-op when `Disabled`.
|
||||
pub fn stamp_session_id_header(&mut self, session_id: &str) {
|
||||
if let Self::Enabled { extra_headers, .. } = self {
|
||||
extra_headers
|
||||
.entry(SESSION_ID_HEADER.to_string())
|
||||
.or_insert_with(|| session_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_gen_enabled(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
|
|
@ -506,6 +520,44 @@ mod tests {
|
|||
assert!(!ImageGenConfig::Disabled.has_credentials());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stamp_session_id_header_sets_and_preserves() {
|
||||
let mk = |headers: indexmap::IndexMap<String, String>| ImageGenConfig::Enabled {
|
||||
api_key: "k".into(),
|
||||
base_url: "https://api.x.ai/v1".into(),
|
||||
extra_headers: headers,
|
||||
image_gen_enabled: true,
|
||||
image_edit_enabled: true,
|
||||
model_override: None,
|
||||
edit_model_override: None,
|
||||
tier_restricted: false,
|
||||
};
|
||||
let hdrs = |cfg: &ImageGenConfig| match cfg {
|
||||
ImageGenConfig::Enabled { extra_headers, .. } => extra_headers.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut cfg = mk(indexmap::IndexMap::new());
|
||||
cfg.stamp_session_id_header("sess-123");
|
||||
assert_eq!(
|
||||
hdrs(&cfg).get(SESSION_ID_HEADER).map(String::as_str),
|
||||
Some("sess-123")
|
||||
);
|
||||
|
||||
let mut preset = indexmap::IndexMap::new();
|
||||
preset.insert(SESSION_ID_HEADER.to_string(), "caller-set".to_string());
|
||||
let mut cfg = mk(preset);
|
||||
cfg.stamp_session_id_header("sess-123");
|
||||
assert_eq!(
|
||||
hdrs(&cfg).get(SESSION_ID_HEADER).map(String::as_str),
|
||||
Some("caller-set")
|
||||
);
|
||||
|
||||
let mut disabled = ImageGenConfig::Disabled;
|
||||
disabled.stamp_session_id_header("sess-123");
|
||||
assert!(!disabled.has_credentials());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_selects_model_from_override() {
|
||||
let mk = |model_override: Option<&str>| ImageGenConfig::Enabled {
|
||||
|
|
|
|||
|
|
@ -691,6 +691,16 @@ impl VideoGenConfig {
|
|||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, Self::Enabled { .. })
|
||||
}
|
||||
|
||||
/// Stamp [`super::image_gen::SESSION_ID_HEADER`] onto `extra_headers`.
|
||||
/// A caller-provided value is never overwritten. No-op when `Disabled`.
|
||||
pub fn stamp_session_id_header(&mut self, session_id: &str) {
|
||||
if let Self::Enabled { extra_headers, .. } = self {
|
||||
extra_headers
|
||||
.entry(super::image_gen::SESSION_ID_HEADER.to_string())
|
||||
.or_insert_with(|| session_id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prose returned to the model (as a normal, successful tool result) when a
|
||||
|
|
|
|||
|
|
@ -265,8 +265,9 @@ mod tests {
|
|||
// to_prompt_format() is a passthrough — it must NOT add another header.
|
||||
let mut bash = make_bash(0, "hello world\n");
|
||||
// Pre-bake DEFAULT (what BashTool::run() does)
|
||||
bash.output_for_prompt =
|
||||
crate::implementations::grok_build::bash::format_default_prompt(&bash);
|
||||
bash.output_for_prompt = crate::implementations::grok_build::bash::format_default_prompt(
|
||||
&bash, /* append_noop_reminder */ true,
|
||||
);
|
||||
assert!(bash.output_for_prompt.starts_with("exit: 0"));
|
||||
|
||||
// Concise post-processing (what BashConciseTool::run() does)
|
||||
|
|
|
|||
|
|
@ -322,10 +322,17 @@ impl xai_tool_runtime::Tool for SearchTool {
|
|||
} else {
|
||||
"partial"
|
||||
};
|
||||
let note = if snapshot.is_ready {
|
||||
None
|
||||
} else {
|
||||
let note = if !snapshot.is_ready {
|
||||
Some("Some MCP servers are still connecting. Results may be incomplete.")
|
||||
} else if snapshot.total_hidden_tools == 0 && result_groups.is_empty() {
|
||||
// Ready but empty: help distinguish "MCP not set up / inheritance
|
||||
// off" from a query that simply matched nothing. Wording is
|
||||
// source-agnostic: search_tool runs in parent and subagent sessions.
|
||||
Some(
|
||||
"No MCP tools are available in this session. Connect MCP servers here, or if this is a subagent, check the agent's mcpInheritance.",
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = serde_json::json!({
|
||||
|
|
@ -420,6 +427,53 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_tool_ready_empty_catalog_includes_guidance_note() {
|
||||
let resources = crate::types::resources::Resources::default().into_shared();
|
||||
resources
|
||||
.lock()
|
||||
.await
|
||||
.insert(ToolIndex(std::sync::Arc::new(StaticToolIndex {
|
||||
snapshot: SearchSnapshot {
|
||||
results: vec![],
|
||||
total_hidden_tools: 0,
|
||||
is_ready: true,
|
||||
},
|
||||
})));
|
||||
let mut ctx =
|
||||
xai_tool_runtime::ToolCallContext::new(xai_tool_protocol::ToolCallId::new_v7());
|
||||
ctx.extensions.insert(resources);
|
||||
|
||||
let output = SearchTool
|
||||
.run(
|
||||
ctx,
|
||||
SearchToolInput {
|
||||
query: "confluence".into(),
|
||||
limit: Some(5),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let ToolOutput::SearchTool(output) = output else {
|
||||
panic!("expected search tool output");
|
||||
};
|
||||
let json: serde_json::Value = serde_json::from_str(&output.content).unwrap();
|
||||
assert_eq!(json["status"], "ready");
|
||||
assert_eq!(json["total_hidden_tools"], 0);
|
||||
assert!(json["results"].as_array().unwrap().is_empty());
|
||||
let note = json["note"]
|
||||
.as_str()
|
||||
.expect("empty ready catalog should set note");
|
||||
assert!(
|
||||
note.contains("Connect MCP servers") && note.contains("mcpInheritance"),
|
||||
"expected source-agnostic guidance about connecting servers / mcpInheritance, got: {note}"
|
||||
);
|
||||
assert!(
|
||||
!note.contains("parent session"),
|
||||
"must not assume a parent session (tool is shared with top-level sessions), got: {note}"
|
||||
);
|
||||
}
|
||||
|
||||
// -- truncate_description tests --
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -983,7 +983,7 @@ impl ToolRegistryBuilder {
|
|||
resources.insert(crate::types::resources::Cwd(cwd.clone()));
|
||||
resources.insert(crate::types::resources::SessionFolder(ctx.session_folder));
|
||||
resources.insert(crate::types::resources::SessionEnv(ctx.session_env));
|
||||
if let Some(owner_session_id) = ctx.owner_session_id {
|
||||
if let Some(owner_session_id) = ctx.owner_session_id.clone() {
|
||||
resources.insert(crate::types::resources::OwnerSessionId(owner_session_id));
|
||||
}
|
||||
if let Some(subagent) = ctx.subagent {
|
||||
|
|
@ -1022,9 +1022,15 @@ impl ToolRegistryBuilder {
|
|||
if let Some(lsp) = ctx.lsp {
|
||||
resources.insert(lsp);
|
||||
}
|
||||
if ctx.image_gen_config.has_credentials() {
|
||||
let mut image_gen_config = ctx.image_gen_config;
|
||||
let mut video_gen_config = ctx.video_gen_config;
|
||||
if let Some(session_id) = &ctx.owner_session_id {
|
||||
image_gen_config.stamp_session_id_header(session_id);
|
||||
video_gen_config.stamp_session_id_header(session_id);
|
||||
}
|
||||
if image_gen_config.has_credentials() {
|
||||
match crate::implementations::grok_build::image_gen::ImageGenClient::new(
|
||||
&ctx.image_gen_config,
|
||||
&image_gen_config,
|
||||
ctx.api_key_provider.clone(),
|
||||
) {
|
||||
Ok(client) => {
|
||||
|
|
@ -1036,9 +1042,9 @@ impl ToolRegistryBuilder {
|
|||
}
|
||||
}
|
||||
}
|
||||
if ctx.video_gen_config.is_enabled() {
|
||||
if video_gen_config.is_enabled() {
|
||||
match crate::implementations::grok_build::video_gen::VideoGenClient::new(
|
||||
&ctx.video_gen_config,
|
||||
&video_gen_config,
|
||||
ctx.api_key_provider.clone(),
|
||||
) {
|
||||
Ok(client) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue