use std::path::Path;
use ag_protocol::ProtocolSchemaInstructionMode;
use crate::agent;
use crate::agent::InstructionDeliveryMode;
use crate::app_server::{AppServerError, AppServerTurnRequest};
use crate::channel::AgentRequestKind;
use crate::model::turn_prompt::{TurnPrompt, TurnPromptTextSource};
pub(crate) fn read_latest_replay_transcript(request: &AppServerTurnRequest) -> Option<String> {
if let Some(live_transcript) = &request.live_transcript
&& let Some(transcript_text) = live_transcript.replay_text()
&& !transcript_text.trim().is_empty()
{
return Some(transcript_text);
}
request.replay_transcript.clone()
}
pub(crate) fn turn_prompt_for_runtime(
prompt: impl Into<TurnPrompt>,
request_kind: &AgentRequestKind,
replay_transcript: Option<&str>,
instruction_delivery_mode: InstructionDeliveryMode,
schema_instruction_mode: ProtocolSchemaInstructionMode,
workspace_root: &Path,
) -> Result<TurnPrompt, AppServerError> {
let prompt = prompt.into();
let agent_prompt = prompt.agent_text();
let turn_prompt = agent::prepare_prompt_text(agent::PromptPreparationRequest {
instruction_delivery_mode,
prompt: &agent_prompt,
protocol_profile: request_kind.protocol_profile(),
replay_transcript,
schema_instruction_mode,
workspace_root,
})
.map_err(|error| AppServerError::PromptRender(error.to_string()))?;
Ok(TurnPrompt {
attachments: prompt.attachments,
text: turn_prompt,
text_source: TurnPromptTextSource::AgentData,
})
}
pub(crate) fn instruction_delivery_mode_for_runtime(
request: &AppServerTurnRequest,
runtime_provider_conversation_id: Option<&str>,
should_replay_transcript: bool,
) -> InstructionDeliveryMode {
agent::plan_app_server_instruction_delivery(
&request.request_kind,
runtime_provider_conversation_id,
request.persisted_instruction_conversation_id.as_deref(),
should_replay_transcript,
)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::Arc;
use super::*;
use crate::channel::LiveTranscript;
use crate::model::agent::ReasoningLevel;
const TEST_WORKSPACE_ROOT: &str = "/tmp/agentty-wt/session-1";
#[derive(Debug)]
struct TestLiveTranscript {
text: String,
}
impl LiveTranscript for TestLiveTranscript {
fn replay_text(&self) -> Option<String> {
Some(self.text.clone())
}
}
fn live_transcript(text: &str) -> Arc<dyn LiveTranscript> {
Arc::new(TestLiveTranscript {
text: text.to_string(),
})
}
fn persisted_instruction_conversation_id_for_session_turn(
provider_conversation_id: Option<&str>,
) -> Option<String> {
agent::normalize_instruction_conversation_id(provider_conversation_id)
}
#[test]
fn read_latest_replay_transcript_prefers_live_source() {
let request = AppServerTurnRequest {
folder: PathBuf::from("/tmp/test"),
live_transcript: Some(live_transcript("live content")),
main_checkout_root: None,
model: "test-model".to_string(),
prompt: TurnPrompt::from("hello"),
provider_conversation_id: None,
persisted_instruction_conversation_id: None,
reasoning_level: ReasoningLevel::default(),
request_kind: AgentRequestKind::SessionStart,
replay_transcript: None,
session_id: "test-session".to_string(),
};
let output = read_latest_replay_transcript(&request);
assert_eq!(output, Some("live content".to_string()));
}
#[test]
fn read_latest_replay_transcript_falls_back_when_live_source_is_empty() {
let request = AppServerTurnRequest {
folder: PathBuf::from("/tmp/test"),
live_transcript: Some(live_transcript(" ")),
main_checkout_root: None,
model: "test-model".to_string(),
prompt: TurnPrompt::from("hello"),
provider_conversation_id: None,
persisted_instruction_conversation_id: None,
reasoning_level: ReasoningLevel::default(),
request_kind: AgentRequestKind::SessionStart,
replay_transcript: Some("queued transcript".to_string()),
session_id: "test-session".to_string(),
};
let output = read_latest_replay_transcript(&request);
assert_eq!(output, Some("queued transcript".to_string()));
}
#[test]
fn read_latest_replay_transcript_returns_none_when_no_replay_text() {
let request = AppServerTurnRequest {
folder: PathBuf::from("/tmp/test"),
live_transcript: None,
main_checkout_root: None,
model: "test-model".to_string(),
prompt: TurnPrompt::from("hello"),
provider_conversation_id: None,
persisted_instruction_conversation_id: None,
reasoning_level: ReasoningLevel::default(),
request_kind: AgentRequestKind::SessionStart,
replay_transcript: None,
session_id: "test-session".to_string(),
};
let output = read_latest_replay_transcript(&request);
assert!(output.is_none());
}
#[test]
fn turn_prompt_for_runtime_includes_protocol_preamble() {
let prompt = TurnPrompt::from("fix the bug");
let request_kind = AgentRequestKind::SessionStart;
let result = turn_prompt_for_runtime(
prompt,
&request_kind,
None,
InstructionDeliveryMode::BootstrapFull,
ProtocolSchemaInstructionMode::PromptSchema,
Path::new(TEST_WORKSPACE_ROOT),
);
let turn_prompt = result.expect("prompt rendering should succeed");
assert!(turn_prompt.text.contains("fix the bug"));
assert!(turn_prompt.text.contains("Structured response protocol:"));
assert!(turn_prompt.text.contains("Anything outside that"));
assert!(turn_prompt.text.contains("root is read-only."));
}
#[test]
fn turn_prompt_for_runtime_omits_full_schema_for_transport_schema_mode() {
let prompt = TurnPrompt::from("fix the bug");
let request_kind = AgentRequestKind::SessionStart;
let result = turn_prompt_for_runtime(
prompt,
&request_kind,
None,
InstructionDeliveryMode::BootstrapFull,
ProtocolSchemaInstructionMode::TransportSchema,
Path::new(TEST_WORKSPACE_ROOT),
);
let turn_prompt = result.expect("prompt rendering should succeed");
assert!(turn_prompt.text.contains("Structured response protocol:"));
assert!(
turn_prompt
.text
.contains("provider enforces Agentty's response JSON schema")
);
assert!(!turn_prompt.text.contains("Authoritative JSON Schema:"));
}
#[test]
fn turn_prompt_for_runtime_uses_compact_refresh_reminder_for_delta_only() {
let prompt = TurnPrompt::from("continue the fix");
let request_kind = AgentRequestKind::SessionResume;
let result = turn_prompt_for_runtime(
prompt,
&request_kind,
None,
InstructionDeliveryMode::DeltaOnly,
ProtocolSchemaInstructionMode::PromptSchema,
Path::new(TEST_WORKSPACE_ROOT),
);
let turn_prompt = result.expect("prompt rendering should succeed");
assert!(turn_prompt.text.contains("Protocol refresh reminder:"));
assert!(!turn_prompt.text.contains("Authoritative JSON Schema:"));
}
#[test]
fn turn_prompt_for_runtime_rewrites_user_at_lookups_for_agent_delivery() {
let prompt = TurnPrompt::from("review @src/main.rs");
let request_kind = AgentRequestKind::SessionStart;
let result = turn_prompt_for_runtime(
prompt,
&request_kind,
None,
InstructionDeliveryMode::BootstrapFull,
ProtocolSchemaInstructionMode::PromptSchema,
Path::new(TEST_WORKSPACE_ROOT),
);
let turn_prompt = result.expect("prompt rendering should succeed");
assert!(turn_prompt.text.contains("\"src/main.rs\""));
assert!(!turn_prompt.text.contains("@src/main.rs"));
assert!(!turn_prompt.text.contains("looked/up/"));
}
#[test]
fn turn_prompt_for_runtime_preserves_generated_at_tokens_for_agent_data() {
let prompt = TurnPrompt::from_agent_data(
"Review this diff:\n```diff\n+@dataclass\n+class Config:\n+ pass\n```".to_string(),
);
let request_kind = AgentRequestKind::UtilityPrompt;
let result = turn_prompt_for_runtime(
prompt,
&request_kind,
None,
InstructionDeliveryMode::BootstrapFull,
ProtocolSchemaInstructionMode::PromptSchema,
Path::new(TEST_WORKSPACE_ROOT),
);
let turn_prompt = result.expect("prompt rendering should succeed");
assert!(turn_prompt.text.contains("+@dataclass"));
assert!(!turn_prompt.text.contains("+\"dataclass\""));
}
#[test]
fn instruction_delivery_mode_for_runtime_reuses_matching_bootstrap_state() {
let request = AppServerTurnRequest {
folder: PathBuf::from("/tmp/test"),
live_transcript: None,
main_checkout_root: None,
model: "test-model".to_string(),
prompt: TurnPrompt::from("hello"),
provider_conversation_id: Some("thread-123".to_string()),
persisted_instruction_conversation_id:
persisted_instruction_conversation_id_for_session_turn(Some("thread-123")),
reasoning_level: ReasoningLevel::default(),
request_kind: AgentRequestKind::SessionResume,
replay_transcript: None,
session_id: "test-session".to_string(),
};
let delivery_mode =
instruction_delivery_mode_for_runtime(&request, Some("thread-123"), false);
assert_eq!(delivery_mode, InstructionDeliveryMode::DeltaOnly);
}
}