Synced from monorepo
Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
This commit is contained in:
parent
3af4d5d398
commit
a5727c5960
482 changed files with 37627 additions and 13402 deletions
|
|
@ -887,10 +887,7 @@ impl AgentBuilder {
|
|||
}
|
||||
let matched = removed.iter().any(|&id| tool_id_eq(d, id));
|
||||
if !matched {
|
||||
tracing::warn!(
|
||||
agent = % definition.name, tool = % d,
|
||||
"disallowedTools entry matched nothing"
|
||||
);
|
||||
tracing::warn!(agent = %definition.name, tool = %d, "disallowedTools entry matched nothing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -933,8 +930,8 @@ impl AgentBuilder {
|
|||
}
|
||||
if !recognized_but_unavailable.is_empty() {
|
||||
tracing::debug!(
|
||||
agent = % definition.name, recognized_but_unavailable = ?
|
||||
recognized_but_unavailable,
|
||||
agent = %definition.name,
|
||||
recognized_but_unavailable = ?recognized_but_unavailable,
|
||||
"tools allowlist named recognized tools that aren't enabled; ignoring them"
|
||||
);
|
||||
}
|
||||
|
|
@ -945,14 +942,12 @@ impl AgentBuilder {
|
|||
|| (has_agent_entry && task_deps.contains(&short_tool_name(&tc.id)))
|
||||
|| matches!(tc.kind, Some(ToolKind::SearchTool | ToolKind::UseTool))
|
||||
});
|
||||
tracing::debug!(
|
||||
agent = % definition.name, allowed = ? definition.tools,
|
||||
"tools allowlist applied"
|
||||
);
|
||||
tracing::debug!(agent = %definition.name, allowed = ?definition.tools, "tools allowlist applied");
|
||||
} else {
|
||||
tracing::warn!(
|
||||
agent = % definition.name, unresolved = ? unresolved, allowed = ?
|
||||
definition.tools,
|
||||
agent = %definition.name,
|
||||
unresolved = ?unresolved,
|
||||
allowed = ?definition.tools,
|
||||
"tools allowlist had unmappable entries; keeping full grok toolset"
|
||||
);
|
||||
}
|
||||
|
|
@ -1195,13 +1190,15 @@ impl AgentBuilder {
|
|||
let mut hosted_tools = Vec::new();
|
||||
if use_backend_search {
|
||||
if web_search_enabled && definition.hosted_tool_allowed("web_search") {
|
||||
hosted_tools.push(xai_grok_sampling_types::HostedTool::WebSearch {
|
||||
allowed_domains: None,
|
||||
});
|
||||
hosted_tools.push(xai_grok_sampling_types::HostedTool::WebSearch { options: None });
|
||||
}
|
||||
if definition.hosted_tool_allowed("x_search") {
|
||||
hosted_tools.push(xai_grok_sampling_types::HostedTool::XSearch);
|
||||
hosted_tools.push(xai_grok_sampling_types::HostedTool::XSearch { options: None });
|
||||
}
|
||||
xai_grok_sampling_types::apply_tool_overrides(
|
||||
&mut hosted_tools,
|
||||
definition.tool_overrides.as_ref(),
|
||||
);
|
||||
}
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let tool_bridge = Arc::new(tool_bridge);
|
||||
|
|
@ -1485,16 +1482,14 @@ mod tests {
|
|||
&subagents,
|
||||
&["zeta".to_string(), "alpha".to_string(), "alpha".to_string()],
|
||||
);
|
||||
assert!(
|
||||
desc
|
||||
.contains("If the user explicitly asks for the model of a subagent/task, you may ONLY use model slugs from this list:\n\
|
||||
assert!(desc.contains(
|
||||
"If the user explicitly asks for the model of a subagent/task, you may ONLY use model slugs from this list:\n\
|
||||
- alpha\n\
|
||||
- zeta")
|
||||
);
|
||||
assert!(
|
||||
desc
|
||||
.contains("If the user does not explicitly request a model, omit `${{ params.task.model }}` to inherit the parent model.")
|
||||
);
|
||||
- zeta"
|
||||
));
|
||||
assert!(desc.contains(
|
||||
"If the user does not explicitly request a model, omit `${{ params.task.model }}` to inherit the parent model."
|
||||
));
|
||||
assert!(!desc.contains("Available model slugs:"));
|
||||
assert!(!desc.contains(concat!("grok", " models")));
|
||||
}
|
||||
|
|
@ -1946,10 +1941,11 @@ mod tests {
|
|||
use xai_grok_tools::types::resources::Params;
|
||||
let mut definition = crate::config::AgentDefinition::default_grok_build();
|
||||
definition.tools = vec!["run_terminal_cmd".into()];
|
||||
let bash_params = serde_json::json!(
|
||||
{ "max_timeout_secs" : 36_000.0, "auto_background_on_timeout" : true,
|
||||
"allow_background_operator" : false, }
|
||||
)
|
||||
let bash_params = serde_json::json!({
|
||||
"max_timeout_secs": 36_000.0,
|
||||
"auto_background_on_timeout": true,
|
||||
"allow_background_operator": false,
|
||||
})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
|
@ -2313,6 +2309,7 @@ mod tests {
|
|||
web_search_enabled: bool,
|
||||
backend_search_enabled: bool,
|
||||
disallowed_tools: &[&str],
|
||||
tool_overrides: Option<xai_grok_sampling_types::ToolOverrides>,
|
||||
) -> crate::agent::Agent {
|
||||
use xai_grok_tools::computer::local::LocalTerminalBackend;
|
||||
use xai_grok_tools::implementations::web_search::WebSearchConfig;
|
||||
|
|
@ -2330,6 +2327,7 @@ mod tests {
|
|||
};
|
||||
let mut def = crate::config::AgentDefinition::default_grok_build();
|
||||
def.disallowed_tools = disallowed_tools.iter().map(|s| s.to_string()).collect();
|
||||
def.tool_overrides = tool_overrides;
|
||||
AgentBuilder::new(
|
||||
std::env::temp_dir(),
|
||||
Arc::new(LocalTerminalBackend::new()),
|
||||
|
|
@ -2344,7 +2342,7 @@ mod tests {
|
|||
}
|
||||
#[tokio::test]
|
||||
async fn disallowed_web_search_strips_function_and_hosted_tools() {
|
||||
let agent = build_with_web_search(true, true, &["web_search"]).await;
|
||||
let agent = build_with_web_search(true, true, &["web_search"], None).await;
|
||||
let hosted = agent.hosted_tools();
|
||||
assert!(
|
||||
!hosted
|
||||
|
|
@ -2355,7 +2353,7 @@ mod tests {
|
|||
assert!(
|
||||
hosted
|
||||
.iter()
|
||||
.any(|t| matches!(t, xai_grok_sampling_types::HostedTool::XSearch)),
|
||||
.any(|t| matches!(t, xai_grok_sampling_types::HostedTool::XSearch { .. })),
|
||||
"XSearch must remain when only web_search is disallowed, got: {hosted:?}"
|
||||
);
|
||||
let has_web_search_fn = agent
|
||||
|
|
@ -2372,7 +2370,7 @@ mod tests {
|
|||
/// hosted tools appear and `backend_search_enabled()` is true.
|
||||
#[tokio::test]
|
||||
async fn hosted_tools_populated_when_backend_search_and_web_search_enabled() {
|
||||
let agent = build_with_web_search(true, true, &[]).await;
|
||||
let agent = build_with_web_search(true, true, &[], None).await;
|
||||
assert!(agent.backend_search_enabled());
|
||||
let hosted = agent.hosted_tools();
|
||||
assert!(
|
||||
|
|
@ -2384,7 +2382,7 @@ mod tests {
|
|||
assert!(
|
||||
hosted
|
||||
.iter()
|
||||
.any(|t| matches!(t, xai_grok_sampling_types::HostedTool::XSearch)),
|
||||
.any(|t| matches!(t, xai_grok_sampling_types::HostedTool::XSearch { .. })),
|
||||
"expected XSearch hosted tool, got: {hosted:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -2392,7 +2390,7 @@ mod tests {
|
|||
/// WebSearch requires the web-search config.
|
||||
#[tokio::test]
|
||||
async fn hosted_tools_only_xsearch_when_web_search_disabled() {
|
||||
let agent = build_with_web_search(false, true, &[]).await;
|
||||
let agent = build_with_web_search(false, true, &[], None).await;
|
||||
let hosted = agent.hosted_tools();
|
||||
assert!(
|
||||
!hosted
|
||||
|
|
@ -2403,7 +2401,7 @@ mod tests {
|
|||
assert!(
|
||||
hosted
|
||||
.iter()
|
||||
.any(|t| matches!(t, xai_grok_sampling_types::HostedTool::XSearch)),
|
||||
.any(|t| matches!(t, xai_grok_sampling_types::HostedTool::XSearch { .. })),
|
||||
"expected XSearch hosted tool, got: {hosted:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -2411,8 +2409,35 @@ mod tests {
|
|||
/// of web-search config.
|
||||
#[tokio::test]
|
||||
async fn hosted_tools_empty_when_backend_search_disabled() {
|
||||
let agent = build_with_web_search(true, false, &[]).await;
|
||||
let agent = build_with_web_search(true, false, &[], None).await;
|
||||
assert!(!agent.backend_search_enabled());
|
||||
assert!(agent.hosted_tools().is_empty());
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn hosted_tools_bake_definition_tool_overrides_into_options() {
|
||||
let x_search = xai_grok_sampling_types::XSearchOptions {
|
||||
date_bound: Some(
|
||||
xai_grok_sampling_types::SearchDateBound::new(None, Some("2024-03-15".into()))
|
||||
.unwrap(),
|
||||
),
|
||||
};
|
||||
let agent = build_with_web_search(
|
||||
true,
|
||||
true,
|
||||
&[],
|
||||
Some(xai_grok_sampling_types::ToolOverrides {
|
||||
x_search: Some(x_search.clone()),
|
||||
web_search: None,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
agent
|
||||
.hosted_tools()
|
||||
.contains(&xai_grok_sampling_types::HostedTool::XSearch {
|
||||
options: Some(x_search),
|
||||
}),
|
||||
"definition tool_overrides must be applied to HostedTool options"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,7 +383,9 @@ fn plan_toolset() -> ToolServerConfig {
|
|||
(&grok_build::ReadFileTool).into(),
|
||||
(&grok_build::ListDirTool).into(),
|
||||
(&grok_build::GrepTool).into(),
|
||||
// (&grok_build::SkillTool).into(),
|
||||
(&grok_build::TodoWriteTool).into(),
|
||||
// search_replace + run_terminal_command intentionally omitted (read-only)
|
||||
],
|
||||
behavior_preset: None,
|
||||
}
|
||||
|
|
@ -397,6 +399,7 @@ fn plan_toolset() -> ToolServerConfig {
|
|||
fn grok_build_plan_toolset() -> ToolServerConfig {
|
||||
ToolServerConfig {
|
||||
tools: vec![
|
||||
// Standard grok-build tools
|
||||
bash_tool_config(),
|
||||
(&grok_build::ReadFileTool).into(),
|
||||
(&grok_build::SearchReplaceTool).into(),
|
||||
|
|
@ -414,6 +417,7 @@ fn grok_build_plan_toolset() -> ToolServerConfig {
|
|||
(&use_tool::UseTool).into(),
|
||||
(&grok_build::UpdateGoalTool).into(),
|
||||
(&grok_build::WorkflowTool).into(),
|
||||
// Plan mode tools
|
||||
(&grok_build::EnterPlanModeTool).into(),
|
||||
(&grok_build::ExitPlanModeTool).into(),
|
||||
(&grok_build::AskUserQuestionTool).into(),
|
||||
|
|
@ -430,33 +434,44 @@ fn grok_build_plan_toolset() -> ToolServerConfig {
|
|||
fn orchestrator_toolset() -> ToolServerConfig {
|
||||
ToolServerConfig {
|
||||
tools: vec![
|
||||
// Research tools
|
||||
bash_tool_config(),
|
||||
(&grok_build::ReadFileTool).into(),
|
||||
(&grok_build::ListDirTool).into(),
|
||||
(&grok_build::GrepTool).into(),
|
||||
// Subagent orchestration
|
||||
task_tool_config(),
|
||||
task_output_tool_config(),
|
||||
wait_tasks_tool_config(),
|
||||
kill_task_tool_config(),
|
||||
// Skills and MCP
|
||||
(&search_tool::SearchTool).into(),
|
||||
(&use_tool::UseTool).into(),
|
||||
// Planning and user interaction
|
||||
(&grok_build::TodoWriteTool).into(),
|
||||
(&grok_build::EnterPlanModeTool).into(),
|
||||
(&grok_build::ExitPlanModeTool).into(),
|
||||
(&grok_build::AskUserQuestionTool).into(),
|
||||
(&grok_build::UpdateGoalTool).into(),
|
||||
(&grok_build::WorkflowTool).into(),
|
||||
// Scheduling and monitoring
|
||||
(&grok_build::SchedulerCreateTool).into(),
|
||||
(&grok_build::SchedulerDeleteTool).into(),
|
||||
(&grok_build::SchedulerListTool).into(),
|
||||
(&grok_build::MonitorTool).into(),
|
||||
// Web tools
|
||||
(&grok_build::WebSearchTool).into(),
|
||||
(&grok_build::WebFetchTool).into(),
|
||||
// Imagine
|
||||
(&grok_build::ImageGenTool).into(),
|
||||
(&grok_build::ImageToVideoTool).into(),
|
||||
(&grok_build::ReferenceToVideoTool).into(),
|
||||
// Memory
|
||||
(&memory::MemorySearchImpl).into(),
|
||||
(&memory::MemoryGetImpl).into(),
|
||||
// Intentionally excluded:
|
||||
// - SearchReplaceTool (no file editing — delegate to subagents)
|
||||
// - OpenCodeWriteTool (no file writing — delegate to subagents)
|
||||
],
|
||||
behavior_preset: None,
|
||||
}
|
||||
|
|
@ -469,6 +484,8 @@ fn orchestrator_toolset() -> ToolServerConfig {
|
|||
fn grok_build_plan_no_subagents_toolset() -> ToolServerConfig {
|
||||
ToolServerConfig {
|
||||
tools: vec![
|
||||
// Standard grok-build tools (minus TaskTool only — KillTaskTool and
|
||||
// TaskOutputTool are kept because BashTool's background mode requires them)
|
||||
bash_tool_config(),
|
||||
(&grok_build::ReadFileTool).into(),
|
||||
(&grok_build::SearchReplaceTool).into(),
|
||||
|
|
@ -485,6 +502,7 @@ fn grok_build_plan_no_subagents_toolset() -> ToolServerConfig {
|
|||
(&use_tool::UseTool).into(),
|
||||
(&grok_build::UpdateGoalTool).into(),
|
||||
(&grok_build::WorkflowTool).into(),
|
||||
// Plan mode tools
|
||||
(&grok_build::EnterPlanModeTool).into(),
|
||||
(&grok_build::ExitPlanModeTool).into(),
|
||||
(&grok_build::AskUserQuestionTool).into(),
|
||||
|
|
@ -517,6 +535,7 @@ fn grok_build_ask_user_toolset() -> ToolServerConfig {
|
|||
(&use_tool::UseTool).into(),
|
||||
(&grok_build::UpdateGoalTool).into(),
|
||||
(&grok_build::WorkflowTool).into(),
|
||||
// Ask user tool (without plan mode)
|
||||
(&grok_build::AskUserQuestionTool).into(),
|
||||
],
|
||||
behavior_preset: None,
|
||||
|
|
@ -792,6 +811,8 @@ pub struct AgentDefinition {
|
|||
/// specific tool before the turn ends.
|
||||
#[serde(default)]
|
||||
pub completion_requirement: Option<CompletionRequirement>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tool_overrides: Option<xai_grok_sampling_types::ToolOverrides>,
|
||||
/// Subagent types this agent can spawn (derived by builder from `tools`).
|
||||
/// `None` = unrestricted, `Some([t1])` = restricted, `Some([])` = blocked.
|
||||
#[serde(skip)]
|
||||
|
|
@ -1458,6 +1479,7 @@ impl AgentDefinition {
|
|||
session_tools_denylist: None,
|
||||
model: ModelOverride::Inherit,
|
||||
completion_requirement: None,
|
||||
tool_overrides: None,
|
||||
prompt_body: None,
|
||||
system_prompt: TemplateOverride::None,
|
||||
source_path: None,
|
||||
|
|
@ -2026,20 +2048,16 @@ description: Minimal agent
|
|||
let v: McpServerRef = serde_json::from_value(serde_json::json!("slack")).unwrap();
|
||||
assert_eq!(v, McpServerRef::Named("slack".to_string()));
|
||||
let v: McpServerRef =
|
||||
serde_json::from_value(serde_json::json!({ "s" : { "type" : "stdio" } })).unwrap();
|
||||
serde_json::from_value(serde_json::json!({"s": {"type": "stdio"}})).unwrap();
|
||||
assert!(matches!(v, McpServerRef::Inline { ref name, .. } if name == "s"));
|
||||
let v: McpServerRef =
|
||||
serde_json::from_value(serde_json::json!({ "name" : "s", "type" : "stdio" })).unwrap();
|
||||
serde_json::from_value(serde_json::json!({"name": "s", "type": "stdio"})).unwrap();
|
||||
assert!(matches!(v, McpServerRef::Inline { ref name, .. } if name == "s"));
|
||||
assert!(
|
||||
serde_json::from_value::<McpServerRef>(serde_json::json!({ "type" :
|
||||
"stdio" }))
|
||||
.is_err()
|
||||
serde_json::from_value::<McpServerRef>(serde_json::json!({"type": "stdio"})).is_err()
|
||||
);
|
||||
assert!(serde_json::from_value::<McpServerRef>(serde_json::json!(42)).is_err());
|
||||
assert!(
|
||||
serde_json::from_value::<McpServerRef>(serde_json::json!({ "s" : "bad" })).is_err()
|
||||
);
|
||||
assert!(serde_json::from_value::<McpServerRef>(serde_json::json!({"s": "bad"})).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn memory_scope_resolve_dir() {
|
||||
|
|
@ -2255,9 +2273,10 @@ description: Test default tool config
|
|||
}
|
||||
#[test]
|
||||
fn test_from_json_minimal() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "acp-agent", "description" : "An agent from ACP" }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "acp-agent",
|
||||
"description": "An agent from ACP"
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
assert_eq!(def.name, "acp-agent");
|
||||
assert_eq!(def.description, "An agent from ACP");
|
||||
|
|
@ -2268,11 +2287,14 @@ description: Test default tool config
|
|||
}
|
||||
#[test]
|
||||
fn test_from_json_has_default_toolset_with_task_tool() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "grok-build", "description" : "Multi-surface coding agent.",
|
||||
"promptMode" : "extend", "permissionMode" : "dontAsk", "agentsMd" : true,
|
||||
"promptBody" : "You are a coding assistant." }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "grok-build",
|
||||
"description": "Multi-surface coding agent.",
|
||||
"promptMode": "extend",
|
||||
"permissionMode": "dontAsk",
|
||||
"agentsMd": true,
|
||||
"promptBody": "You are a coding assistant."
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
let task_tool_id = "GrokBuild:task";
|
||||
assert!(
|
||||
|
|
@ -2288,10 +2310,11 @@ description: Test default tool config
|
|||
}
|
||||
#[test]
|
||||
fn test_from_json_with_prompt_body() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "custom-agent", "description" : "Agent with prompt body",
|
||||
"promptBody" : "You are a specialized coding assistant.\n\nFocus on Rust." }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "custom-agent",
|
||||
"description": "Agent with prompt body",
|
||||
"promptBody": "You are a specialized coding assistant.\n\nFocus on Rust."
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
assert_eq!(def.name, "custom-agent");
|
||||
assert_eq!(
|
||||
|
|
@ -2301,20 +2324,23 @@ description: Test default tool config
|
|||
}
|
||||
#[test]
|
||||
fn test_from_json_with_permission_mode() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "auto-accept-agent", "description" :
|
||||
"Agent with dontAsk permission mode", "permissionMode" : "dontAsk",
|
||||
"promptBody" : "## Auto-accept Mode" }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "auto-accept-agent",
|
||||
"description": "Agent with dontAsk permission mode",
|
||||
"permissionMode": "dontAsk",
|
||||
"promptBody": "## Auto-accept Mode"
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
assert_eq!(def.permission_mode, PermissionMode::DontAsk);
|
||||
assert_eq!(def.prompt_body.as_deref(), Some("## Auto-accept Mode"));
|
||||
}
|
||||
#[test]
|
||||
fn test_from_json_empty_prompt_body_is_none() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "test", "description" : "Test", "promptBody" : " " }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "test",
|
||||
"description": "Test",
|
||||
"promptBody": " "
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
assert!(
|
||||
def.prompt_body.is_none(),
|
||||
|
|
@ -2323,16 +2349,20 @@ description: Test default tool config
|
|||
}
|
||||
#[test]
|
||||
fn test_from_json_missing_required_fields() {
|
||||
let json = serde_json::json!({ "description" : "Missing name" });
|
||||
let json = serde_json::json!({
|
||||
"description": "Missing name"
|
||||
});
|
||||
let result = AgentDefinition::from_json(&json);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn test_from_json_ignores_unknown_fields() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "test", "description" : "Test", "unknownField" : "value",
|
||||
"futureFeature" : true }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "test",
|
||||
"description": "Test",
|
||||
"unknownField": "value",
|
||||
"futureFeature": true
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
assert_eq!(def.name, "test");
|
||||
}
|
||||
|
|
@ -2410,9 +2440,11 @@ description: Test default tool config
|
|||
}
|
||||
#[test]
|
||||
fn test_model_override_in_json() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "test", "description" : "Test", "model" : "grok-code-fast-1" }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "test",
|
||||
"description": "Test",
|
||||
"model": "grok-code-fast-1"
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
assert_eq!(
|
||||
def.model,
|
||||
|
|
@ -2518,10 +2550,11 @@ description: Test default tool config
|
|||
}
|
||||
#[test]
|
||||
fn mcp_inheritance_round_trips_via_json() {
|
||||
let json = serde_json::json!(
|
||||
{ "name" : "t", "description" : "t", "mcpInheritance" : { "named" : ["a",
|
||||
"b"] } }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"name": "t",
|
||||
"description": "t",
|
||||
"mcpInheritance": {"named": ["a", "b"]}
|
||||
});
|
||||
let def = AgentDefinition::from_json(&json).unwrap();
|
||||
assert_eq!(
|
||||
def.mcp_inheritance,
|
||||
|
|
|
|||
|
|
@ -235,18 +235,19 @@ impl PromptContext {
|
|||
/// These are the agent-specific values that get merged with the
|
||||
/// tool context in `TemplateRenderer::render_with_extra()`.
|
||||
pub fn placeholders(&self) -> serde_json::Value {
|
||||
serde_json::json!(
|
||||
{ "memory_enabled" : self.memory_enabled, "memory_global_path" : self
|
||||
.memory_global_path.as_deref().unwrap_or(""), "memory_workspace_path" : self
|
||||
.memory_workspace_path.as_deref().unwrap_or(""), "role_instructions" : self
|
||||
.role_instructions.as_deref().unwrap_or(""), "persona_instructions" : self
|
||||
.persona_instructions.as_deref().unwrap_or(""), "os_name" : self.os_name
|
||||
.as_deref().unwrap_or(""), "shell_path" : self.shell_path.as_deref()
|
||||
.unwrap_or(""), "working_directory" : self.working_directory.as_deref()
|
||||
.unwrap_or(""), "current_date" : self.current_date.as_deref().unwrap_or(""),
|
||||
"is_non_interactive" : self.is_non_interactive, "system_prompt_label" : self
|
||||
.system_prompt_label.as_str(), }
|
||||
)
|
||||
serde_json::json!({
|
||||
"memory_enabled": self.memory_enabled,
|
||||
"memory_global_path": self.memory_global_path.as_deref().unwrap_or(""),
|
||||
"memory_workspace_path": self.memory_workspace_path.as_deref().unwrap_or(""),
|
||||
"role_instructions": self.role_instructions.as_deref().unwrap_or(""),
|
||||
"persona_instructions": self.persona_instructions.as_deref().unwrap_or(""),
|
||||
"os_name": self.os_name.as_deref().unwrap_or(""),
|
||||
"shell_path": self.shell_path.as_deref().unwrap_or(""),
|
||||
"working_directory": self.working_directory.as_deref().unwrap_or(""),
|
||||
"current_date": self.current_date.as_deref().unwrap_or(""),
|
||||
"is_non_interactive": self.is_non_interactive,
|
||||
"system_prompt_label": self.system_prompt_label.as_str(),
|
||||
})
|
||||
}
|
||||
/// Render the full system prompt via `ToolBridge`.
|
||||
///
|
||||
|
|
@ -779,13 +780,24 @@ mod tests {
|
|||
}
|
||||
fn base_template_ctx() -> minijinja::Value {
|
||||
minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => true,
|
||||
role_instructions => "", persona_instructions => "", tools =>
|
||||
minijinja::context! { by_kind => minijinja::context! { read =>
|
||||
"hashline_read", edit => "hashline_edit", search => "hashline_grep", execute
|
||||
=> "run_terminal_cmd", background_task_action => "get_task_output",
|
||||
memory_search => "memory_search", memory_get => "memory_get", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => true,
|
||||
role_instructions => "",
|
||||
persona_instructions => "",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "hashline_read",
|
||||
edit => "hashline_edit",
|
||||
search => "hashline_grep",
|
||||
execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output",
|
||||
memory_search => "memory_search",
|
||||
memory_get => "memory_get",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
|
|
@ -842,13 +854,23 @@ mod tests {
|
|||
#[test]
|
||||
fn child_rendered_prompt_includes_role_and_persona_sections() {
|
||||
let ctx = minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => false,
|
||||
role_instructions => "Follow Rust conventions", persona_instructions =>
|
||||
"You are a code reviewer", tools => minijinja::context! { by_kind =>
|
||||
minijinja::context! { read => "hashline_read", edit => "hashline_edit",
|
||||
search => "hashline_grep", execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => false,
|
||||
role_instructions => "Follow Rust conventions",
|
||||
persona_instructions => "You are a code reviewer",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "hashline_read",
|
||||
edit => "hashline_edit",
|
||||
search => "hashline_grep",
|
||||
execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output",
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
let rendered = render_subagent_template(ctx);
|
||||
assert!(rendered.contains("<role-instructions>"));
|
||||
|
|
@ -899,11 +921,20 @@ mod tests {
|
|||
#[test]
|
||||
fn child_rendered_prompt_omits_background_tasks_without_execute() {
|
||||
let ctx = minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => false,
|
||||
role_instructions => "", persona_instructions => "", tools =>
|
||||
minijinja::context! { by_kind => minijinja::context! { read =>
|
||||
"hashline_read", edit => "hashline_edit", search => "hashline_grep", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => false,
|
||||
role_instructions => "",
|
||||
persona_instructions => "",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "hashline_read",
|
||||
edit => "hashline_edit",
|
||||
search => "hashline_grep",
|
||||
}
|
||||
},
|
||||
};
|
||||
let rendered = render_subagent_template(ctx);
|
||||
assert!(
|
||||
|
|
@ -927,12 +958,22 @@ mod tests {
|
|||
#[test]
|
||||
fn child_rendered_prompt_omits_code_change_rules_without_edit_tools() {
|
||||
let ctx = minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => false,
|
||||
role_instructions => "", persona_instructions => "", tools =>
|
||||
minijinja::context! { by_kind => minijinja::context! { read =>
|
||||
"hashline_read", search => "hashline_grep", execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => false,
|
||||
role_instructions => "",
|
||||
persona_instructions => "",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "hashline_read",
|
||||
search => "hashline_grep",
|
||||
execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output",
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
let rendered = render_subagent_template(ctx);
|
||||
assert!(
|
||||
|
|
@ -955,12 +996,22 @@ mod tests {
|
|||
#[test]
|
||||
fn rendered_prompt_size_read_only() {
|
||||
let ctx = minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => false,
|
||||
role_instructions => "", persona_instructions => "", tools =>
|
||||
minijinja::context! { by_kind => minijinja::context! { read =>
|
||||
"hashline_read", search => "hashline_grep", execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => false,
|
||||
role_instructions => "",
|
||||
persona_instructions => "",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "hashline_read",
|
||||
search => "hashline_grep",
|
||||
execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output",
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
let rendered = render_subagent_template(ctx);
|
||||
assert!(
|
||||
|
|
@ -979,12 +1030,22 @@ mod tests {
|
|||
#[test]
|
||||
fn child_rendered_prompt_omits_edit_references_without_edit_tool() {
|
||||
let ctx = minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => false,
|
||||
role_instructions => "", persona_instructions => "", tools =>
|
||||
minijinja::context! { by_kind => minijinja::context! { read => "read_file",
|
||||
search => "grep", execute => "run_terminal_cmd", background_task_action =>
|
||||
"get_task_output", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => false,
|
||||
role_instructions => "",
|
||||
persona_instructions => "",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "read_file",
|
||||
search => "grep",
|
||||
execute => "run_terminal_cmd",
|
||||
background_task_action => "get_task_output",
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
let rendered = render_subagent_template(ctx);
|
||||
assert!(
|
||||
|
|
@ -995,11 +1056,20 @@ mod tests {
|
|||
#[test]
|
||||
fn child_rendered_prompt_omits_execute_references_without_execute_tool() {
|
||||
let ctx = minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => false,
|
||||
role_instructions => "", persona_instructions => "", tools =>
|
||||
minijinja::context! { by_kind => minijinja::context! { read => "read_file",
|
||||
edit => "search_replace", search => "grep", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => false,
|
||||
role_instructions => "",
|
||||
persona_instructions => "",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "read_file",
|
||||
edit => "search_replace",
|
||||
search => "grep",
|
||||
}
|
||||
},
|
||||
};
|
||||
let rendered = render_subagent_template(ctx);
|
||||
assert!(
|
||||
|
|
@ -1014,11 +1084,19 @@ mod tests {
|
|||
#[test]
|
||||
fn child_rendered_prompt_omits_both_edit_and_execute_references() {
|
||||
let ctx = minijinja::context! {
|
||||
os_name => "linux", shell_path => "/bin/bash", working_directory =>
|
||||
"/workspace", current_date => "2026-03-26", memory_enabled => false,
|
||||
role_instructions => "", persona_instructions => "", tools =>
|
||||
minijinja::context! { by_kind => minijinja::context! { read => "read_file",
|
||||
search => "grep", } },
|
||||
os_name => "linux",
|
||||
shell_path => "/bin/bash",
|
||||
working_directory => "/workspace",
|
||||
current_date => "2026-03-26",
|
||||
memory_enabled => false,
|
||||
role_instructions => "",
|
||||
persona_instructions => "",
|
||||
tools => minijinja::context! {
|
||||
by_kind => minijinja::context! {
|
||||
read => "read_file",
|
||||
search => "grep",
|
||||
}
|
||||
},
|
||||
};
|
||||
let rendered = render_subagent_template(ctx);
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
//! workspace overview, optional rules / skills / MCP listings).
|
||||
//!
|
||||
//! `UserMessageTemplate` selects the rendering strategy:
|
||||
//! - `Default` -- the legacy Grok Build prefix (built by the shell layer).
|
||||
//! - `Custom` -- caller-supplied template string (MiniJinja, same delimiters
|
||||
//! as the system prompt templates).
|
||||
//! - `Default`: the legacy Grok Build prefix (built by the shell layer).
|
||||
//! - `Custom`: caller-supplied MiniJinja template string (same delimiters as
|
||||
//! the system prompt templates).
|
||||
//!
|
||||
//! The shell layer gathers session-scoped inputs (cwd, vcs status, rule
|
||||
//! files, skill registry, MCP servers) and hands them to
|
||||
|
|
@ -63,10 +63,8 @@ pub fn normalize_git_status(status: &str) -> Option<String> {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UserMessageTemplate {
|
||||
/// Legacy Grok Build prefix: `<user_info>` + optional `<git_status>`.
|
||||
/// Built directly by the shell layer; this
|
||||
/// renderer returns `None` for `Default` and the caller falls back to
|
||||
/// its own legacy path.
|
||||
/// Legacy Grok Build prefix (`<user_info>` + optional `<git_status>`), built directly by the
|
||||
/// shell layer; the renderer returns `None` and the caller uses its own legacy path.
|
||||
#[default]
|
||||
Default,
|
||||
/// Caller-supplied MiniJinja template string.
|
||||
|
|
@ -76,6 +74,15 @@ impl UserMessageTemplate {
|
|||
pub fn is_cursor(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Whether this template surfaces the session's local date, scoping the date-rollover reminder.
|
||||
/// A `Custom` template omitting [`TODAY_LOCAL_PLACEHOLDER`] is date-free. The substring check can
|
||||
/// only over-keep the reminder, never wrongly suppress a dated session.
|
||||
pub fn surfaces_local_date(&self) -> bool {
|
||||
match self {
|
||||
Self::Default => true,
|
||||
Self::Custom(body) => body.contains(TODAY_LOCAL_PLACEHOLDER),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Backward-compatible deserialization: accepts both the new tagged format
|
||||
/// (`"default"`, `{"custom": "..."}`) and a bare string (treated
|
||||
|
|
@ -205,6 +212,9 @@ pub struct UserMessageContext {
|
|||
/// Used in the skill section's instructional text. Defaults to `"Read"`.
|
||||
pub read_tool_name: String,
|
||||
}
|
||||
/// MiniJinja variable a `Custom` template renders the local date under (pinned to the serialized
|
||||
/// field by `placeholders_carry_today_local_key`).
|
||||
pub const TODAY_LOCAL_PLACEHOLDER: &str = "today_local";
|
||||
/// Typed placeholder bag handed to MiniJinja.
|
||||
///
|
||||
/// Field names here must match `${{ … }}` references in any caller-supplied
|
||||
|
|
@ -318,6 +328,30 @@ mod tests {
|
|||
assert_eq!(v, UserMessageTemplate::Custom("my custom".into()));
|
||||
}
|
||||
#[test]
|
||||
fn placeholders_carry_today_local_key() {
|
||||
let placeholders = UserMessagePlaceholders {
|
||||
workspace_path: "/w".into(),
|
||||
os_family: "macos",
|
||||
shell: "zsh",
|
||||
vcs_root: None,
|
||||
vcs_status: None,
|
||||
today_local: Some("Friday Apr 24, 2026".into()),
|
||||
terminals_folder: None,
|
||||
has_rules: false,
|
||||
workspace_rules: &[],
|
||||
user_rules: &[],
|
||||
skill_listing: String::new(),
|
||||
read_tool_name: "Read".into(),
|
||||
mcp_servers: &[],
|
||||
mcps_root: None,
|
||||
};
|
||||
let json = serde_json::to_value(&placeholders).unwrap();
|
||||
assert!(
|
||||
json.get(TODAY_LOCAL_PLACEHOLDER).is_some(),
|
||||
"the local-date placeholder must serialize under TODAY_LOCAL_PLACEHOLDER"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn template_override_deserialize_custom_map() {
|
||||
let v: UserMessageTemplate =
|
||||
serde_json::from_str(r#"{"custom": "my template body"}"#).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue