roboticus-api 0.11.3

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
//! Protocol guards: InternalProtocol.
//!
//! Also contains the internal protocol detection and stripping helpers used
//! by other modules via `pub(super)` re-exports.

use crate::api::routes::agent::guard_registry::{Guard, GuardContext, GuardId, GuardVerdict};

// ── 11. InternalProtocolGuard ────────────────────────────────────────────

pub(in crate::api::routes::agent) struct InternalProtocolGuard;

impl Guard for InternalProtocolGuard {
    fn id(&self) -> GuardId {
        GuardId::InternalProtocol
    }

    fn is_relevant(&self, _ctx: &GuardContext) -> bool {
        true
    }

    fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
        let lower = content.to_ascii_lowercase();
        if !lower.contains("\"tool_call\"")
            && !lower.contains("\"toolcall\"")
            && !lower.contains("unexecuted_streaming_tool_call")
            && !lower.contains("delegated_subagent=")
            && !lower.contains("selected_subagent=")
            && !lower.contains("subtask ")
            && !(lower.contains("{\"name\":")
                && lower.contains("\"params\":")
                && lower.contains("\"content\":"))
        {
            return GuardVerdict::Pass;
        }

        let stripped = strip_internal_protocol_metadata(content);
        if stripped.is_empty() {
            return GuardVerdict::RetryRequested {
                reason: format!(
                    "Response contained only internal protocol metadata with no \
                     user-facing content. As {}, respond naturally to the user's \
                     message in your own voice. Do not emit tool call JSON, \
                     delegation markers, or internal protocol.",
                    ctx.agent_name
                ),
            };
        }
        GuardVerdict::Rewritten(stripped)
    }
}

// ── Internal protocol detection helpers ──────────────────────────────────

fn is_internal_delegation_metadata_line(line: &str) -> bool {
    let t = line.trim();
    if t.starts_with("delegated_subagent=")
        || t.starts_with("selected_subagent=")
        || t.starts_with("fallback_models=")
        || t.starts_with("notes=")
    {
        return true;
    }
    if let Some(rest) = t.strip_prefix("subtask ") {
        let mut parts = rest.splitn(2, " -> ");
        if let (Some(left), Some(_)) = (parts.next(), parts.next())
            && left.chars().all(|c| c.is_ascii_digit())
        {
            return true;
        }
    }
    false
}

fn is_internal_orchestration_narrative_line(line: &str) -> bool {
    let t = line.trim().to_ascii_lowercase();
    t.starts_with("centralized delegation is sensible")
        || t.starts_with("decomposition gate decision")
        || t.starts_with("expected_utility_margin=")
        || t.starts_with("expected utility margin")
        || t.starts_with("delegation decision:")
        || t.starts_with("rationale:")
        || t.starts_with("subtasks:")
}

fn is_internal_tool_protocol_line(line: &str) -> bool {
    let t = line.trim().to_ascii_lowercase();
    t.contains(r#""tool_call""#)
        || t.starts_with("unexecuted_streaming_tool_call:")
        || t.starts_with("tool_call:")
        || t.starts_with("{\"tool_call\"")
        || t.starts_with("{\"toolcall\"")
        || (t.starts_with('{')
            && t.contains("\"name\":")
            && (t.contains("\"params\":") || t.contains("\"arguments\":")))
}

pub(in crate::api::routes::agent) fn contains_internal_protocol_marker(content: &str) -> bool {
    let lower = content.to_ascii_lowercase();
    let has_raw_tool_json = lower.contains("{\"name\":")
        && lower.contains("\"params\":")
        && lower.contains("\"content\":");
    content.lines().any(|line| {
        is_internal_delegation_metadata_line(line)
            || is_internal_orchestration_narrative_line(line)
            || is_internal_tool_protocol_line(line)
    }) || has_raw_tool_json
}

fn strip_internal_protocol_metadata(content: &str) -> String {
    content
        .lines()
        .filter(|line| {
            !is_internal_delegation_metadata_line(line)
                && !is_internal_orchestration_narrative_line(line)
                && !is_internal_tool_protocol_line(line)
        })
        .collect::<Vec<_>>()
        .join("\n")
        .trim()
        .to_string()
}