Skip to main content

ag_agent/agent/
instruction.rs

1//! Provider-managed instruction bootstrap planning for app-server sessions.
2
3use crate::channel::AgentRequestKind;
4
5/// Normalizes one provider-native conversation id for persisted bootstrap
6/// reuse tracking.
7pub fn normalize_instruction_conversation_id(
8    provider_conversation_id: Option<&str>,
9) -> Option<String> {
10    provider_conversation_id
11        .map(str::trim)
12        .filter(|value| !value.is_empty())
13        .map(ToString::to_string)
14}
15
16/// Prompt-shaping mode used for one app-server turn attempt.
17#[derive(Debug, Clone, Copy, Eq, PartialEq)]
18pub enum InstructionDeliveryMode {
19    /// Send the full instruction contract without transcript replay.
20    BootstrapFull,
21    /// Reuse the existing provider-managed bootstrap and send only a compact
22    /// reminder.
23    DeltaOnly,
24    /// Re-send the full instruction contract while replaying the transcript
25    /// after context loss.
26    BootstrapWithReplay,
27}
28
29/// Plans how one app-server turn should deliver Agentty's instruction
30/// contract.
31pub fn plan_app_server_instruction_delivery(
32    request_kind: &AgentRequestKind,
33    current_provider_conversation_id: Option<&str>,
34    persisted_instruction_conversation_id: Option<&str>,
35    should_replay_session_output: bool,
36) -> InstructionDeliveryMode {
37    if should_replay_session_output {
38        return InstructionDeliveryMode::BootstrapWithReplay;
39    }
40
41    if matches!(
42        request_kind,
43        AgentRequestKind::UtilityPrompt | AgentRequestKind::AccountRead
44    ) {
45        return InstructionDeliveryMode::BootstrapFull;
46    }
47
48    if normalize_instruction_conversation_id(current_provider_conversation_id).as_deref()
49        == persisted_instruction_conversation_id
50    {
51        return InstructionDeliveryMode::DeltaOnly;
52    }
53
54    InstructionDeliveryMode::BootstrapFull
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    /// Reuses the provider-managed bootstrap only when the persisted state
63    /// still matches the active provider conversation.
64    fn test_plan_app_server_instruction_delivery_uses_delta_only_for_matching_state() {
65        // Arrange
66        let persisted_instruction_conversation_id =
67            normalize_instruction_conversation_id(Some("thread-123"));
68
69        // Act
70        let mode = plan_app_server_instruction_delivery(
71            &AgentRequestKind::SessionResume {
72                session_output: None,
73            },
74            Some("thread-123"),
75            persisted_instruction_conversation_id.as_deref(),
76            false,
77        );
78
79        // Assert
80        assert_eq!(mode, InstructionDeliveryMode::DeltaOnly);
81    }
82
83    #[test]
84    /// Forces a replay bootstrap whenever the runtime lost provider-managed
85    /// context for the active turn.
86    fn test_plan_app_server_instruction_delivery_uses_bootstrap_with_replay_after_reset() {
87        // Arrange
88        let persisted_instruction_conversation_id =
89            normalize_instruction_conversation_id(Some("thread-123"));
90
91        // Act
92        let mode = plan_app_server_instruction_delivery(
93            &AgentRequestKind::SessionResume {
94                session_output: Some("previous output".to_string()),
95            },
96            Some("thread-456"),
97            persisted_instruction_conversation_id.as_deref(),
98            true,
99        );
100
101        // Assert
102        assert_eq!(mode, InstructionDeliveryMode::BootstrapWithReplay);
103    }
104
105    #[test]
106    /// Requires a fresh bootstrap when the provider conversation changed.
107    fn test_plan_app_server_instruction_delivery_bootstraps_full_for_new_context() {
108        // Arrange
109        let persisted_instruction_conversation_id =
110            normalize_instruction_conversation_id(Some("thread-123"));
111
112        // Act
113        let mode = plan_app_server_instruction_delivery(
114            &AgentRequestKind::SessionResume {
115                session_output: None,
116            },
117            Some("thread-456"),
118            persisted_instruction_conversation_id.as_deref(),
119            false,
120        );
121
122        // Assert
123        assert_eq!(mode, InstructionDeliveryMode::BootstrapFull);
124    }
125
126    #[test]
127    /// Keeps one-shot utility prompts on the full bootstrap path because they
128    /// do not reuse long-lived provider context.
129    fn test_plan_app_server_instruction_delivery_bootstraps_full_for_utility_prompt() {
130        // Arrange
131        let persisted_instruction_conversation_id =
132            normalize_instruction_conversation_id(Some("thread-123"));
133
134        // Act
135        let mode = plan_app_server_instruction_delivery(
136            &AgentRequestKind::UtilityPrompt,
137            Some("thread-123"),
138            persisted_instruction_conversation_id.as_deref(),
139            false,
140        );
141
142        // Assert
143        assert_eq!(mode, InstructionDeliveryMode::BootstrapFull);
144    }
145}