diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index b30e3073..892afc6a 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -86,6 +86,13 @@ fn phantom_action_detected(text: &str) -> bool { has_action && has_channel } +/// Returns true when the agent response text indicates an intentional silent completion. +/// Matches `NO_REPLY` (exact) and `[SILENT]` (case-insensitive). +fn is_silent_token(text: &str) -> bool { + let trimmed = text.trim(); + trimmed == "NO_REPLY" || trimmed.eq_ignore_ascii_case("[silent]") +} + /// Extra guidance injected after failed tool calls to prevent fabricated follow-up actions. const TOOL_ERROR_GUIDANCE: &str = "[System: One or more tool calls failed. Failed tools did not produce usable data. Do NOT invent missing results, cite nonexistent search results, or pretend failed tools succeeded. If your next steps depend on a failed tool, either retry with a materially different approach or explain the failure to the user and stop. Do not write files, store memory, or take downstream actions based on failed tool outputs.]"; @@ -463,8 +470,9 @@ pub async fn run_agent_loop( crate::reply_directives::parse_directives(&text); let text = cleaned_text; - // NO_REPLY: agent intentionally chose not to reply - if text.trim() == "NO_REPLY" || parsed_directives.silent { + // NO_REPLY / [SILENT]: agent intentionally chose not to reply. + // [SILENT] must not be stored literally — it reinforces silence in future turns. + if is_silent_token(&text) || parsed_directives.silent { debug!(agent = %manifest.name, "Agent chose NO_REPLY/silent — silent completion"); session .messages @@ -1641,8 +1649,9 @@ pub async fn run_agent_loop_streaming( crate::reply_directives::parse_directives(&text); let text = cleaned_text_s; - // NO_REPLY: agent intentionally chose not to reply - if text.trim() == "NO_REPLY" || parsed_directives_s.silent { + // NO_REPLY / [SILENT]: agent intentionally chose not to reply. + // [SILENT] must not be stored literally — it reinforces silence in future turns. + if is_silent_token(&text) || parsed_directives_s.silent { debug!(agent = %manifest.name, "Agent chose NO_REPLY/silent (streaming) — silent completion"); session .messages @@ -4851,4 +4860,36 @@ mod tests { } assert!(!events.is_empty(), "Should have received stream events"); } + + #[test] + fn test_silent_detection_uppercase() { + assert!(is_silent_token("[SILENT]")); + } + + #[test] + fn test_silent_detection_lowercase() { + assert!(is_silent_token("[silent]")); + } + + #[test] + fn test_silent_detection_mixed_case() { + assert!(is_silent_token("[Silent]")); + } + + #[test] + fn test_silent_detection_with_whitespace() { + assert!(is_silent_token(" [SILENT] ")); + } + + #[test] + fn test_silent_detection_no_reply() { + assert!(is_silent_token("NO_REPLY")); + } + + #[test] + fn test_silent_detection_rejects_normal_text() { + assert!(!is_silent_token("Hello, how can I help?")); + assert!(!is_silent_token("SILENT")); + assert!(!is_silent_token("")); + } } diff --git a/crates/openfang-runtime/src/tool_runner.rs b/crates/openfang-runtime/src/tool_runner.rs index 8045ad9b..b2913ff1 100644 --- a/crates/openfang-runtime/src/tool_runner.rs +++ b/crates/openfang-runtime/src/tool_runner.rs @@ -3677,13 +3677,12 @@ mod tests { None, // process_manager ) .await; - // Should NOT be the capability-check denial — it should normalize to file_write - // and pass the capability check. It may fail for other reasons (path validation, - // OS-level errors), but not the agent capability gate. + // Should NOT be the capability-enforcement "Permission denied" — it should + // normalize to file_write and pass the capability check. It may still fail + // for filesystem reasons (e.g. OS "Permission denied (os error 13)"), so we + // check specifically for the capability-gate message. assert!( - !result - .content - .contains("does not have capability to use tool"), + !result.content.contains("Permission denied: agent"), "fs-write should normalize to file_write and pass capability check, got: {}", result.content );