Skip to content

Commit eebb83c

Browse files
authored
Merge pull request #877 from pbranchu/fix/silent-reinforcement
Fix silent reinforcement: recognize [SILENT] token
2 parents 0b59205 + a3cefa4 commit eebb83c

File tree

2 files changed

+50
-10
lines changed

2 files changed

+50
-10
lines changed

crates/openfang-runtime/src/agent_loop.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ fn phantom_action_detected(text: &str) -> bool {
8686
has_action && has_channel
8787
}
8888

89+
/// Returns true when the agent response text indicates an intentional silent completion.
90+
/// Matches `NO_REPLY` (exact) and `[SILENT]` (case-insensitive).
91+
fn is_silent_token(text: &str) -> bool {
92+
let trimmed = text.trim();
93+
trimmed == "NO_REPLY" || trimmed.eq_ignore_ascii_case("[silent]")
94+
}
95+
8996
/// Extra guidance injected after failed tool calls to prevent fabricated follow-up actions.
9097
const TOOL_ERROR_GUIDANCE: &str =
9198
"[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(
463470
crate::reply_directives::parse_directives(&text);
464471
let text = cleaned_text;
465472

466-
// NO_REPLY: agent intentionally chose not to reply
467-
if text.trim() == "NO_REPLY" || parsed_directives.silent {
473+
// NO_REPLY / [SILENT]: agent intentionally chose not to reply.
474+
// [SILENT] must not be stored literally — it reinforces silence in future turns.
475+
if is_silent_token(&text) || parsed_directives.silent {
468476
debug!(agent = %manifest.name, "Agent chose NO_REPLY/silent — silent completion");
469477
session
470478
.messages
@@ -1641,8 +1649,9 @@ pub async fn run_agent_loop_streaming(
16411649
crate::reply_directives::parse_directives(&text);
16421650
let text = cleaned_text_s;
16431651

1644-
// NO_REPLY: agent intentionally chose not to reply
1645-
if text.trim() == "NO_REPLY" || parsed_directives_s.silent {
1652+
// NO_REPLY / [SILENT]: agent intentionally chose not to reply.
1653+
// [SILENT] must not be stored literally — it reinforces silence in future turns.
1654+
if is_silent_token(&text) || parsed_directives_s.silent {
16461655
debug!(agent = %manifest.name, "Agent chose NO_REPLY/silent (streaming) — silent completion");
16471656
session
16481657
.messages
@@ -4851,4 +4860,36 @@ mod tests {
48514860
}
48524861
assert!(!events.is_empty(), "Should have received stream events");
48534862
}
4863+
4864+
#[test]
4865+
fn test_silent_detection_uppercase() {
4866+
assert!(is_silent_token("[SILENT]"));
4867+
}
4868+
4869+
#[test]
4870+
fn test_silent_detection_lowercase() {
4871+
assert!(is_silent_token("[silent]"));
4872+
}
4873+
4874+
#[test]
4875+
fn test_silent_detection_mixed_case() {
4876+
assert!(is_silent_token("[Silent]"));
4877+
}
4878+
4879+
#[test]
4880+
fn test_silent_detection_with_whitespace() {
4881+
assert!(is_silent_token(" [SILENT] "));
4882+
}
4883+
4884+
#[test]
4885+
fn test_silent_detection_no_reply() {
4886+
assert!(is_silent_token("NO_REPLY"));
4887+
}
4888+
4889+
#[test]
4890+
fn test_silent_detection_rejects_normal_text() {
4891+
assert!(!is_silent_token("Hello, how can I help?"));
4892+
assert!(!is_silent_token("SILENT"));
4893+
assert!(!is_silent_token(""));
4894+
}
48544895
}

crates/openfang-runtime/src/tool_runner.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3677,13 +3677,12 @@ mod tests {
36773677
None, // process_manager
36783678
)
36793679
.await;
3680-
// Should NOT be the capability-check denial — it should normalize to file_write
3681-
// and pass the capability check. It may fail for other reasons (path validation,
3682-
// OS-level errors), but not the agent capability gate.
3680+
// Should NOT be the capability-enforcement "Permission denied" — it should
3681+
// normalize to file_write and pass the capability check. It may still fail
3682+
// for filesystem reasons (e.g. OS "Permission denied (os error 13)"), so we
3683+
// check specifically for the capability-gate message.
36833684
assert!(
3684-
!result
3685-
.content
3686-
.contains("does not have capability to use tool"),
3685+
!result.content.contains("Permission denied: agent"),
36873686
"fs-write should normalize to file_write and pass capability check, got: {}",
36883687
result.content
36893688
);

0 commit comments

Comments
 (0)