use crate::gateway::workspace_prompt::{
render_repo_map, render_workspace_prompt_block, repo_map_is_source, repo_map_path_is_skipped,
repo_map_signatures, RepoMapEntry, WorkspacePromptFile, REPO_MAP_MAX_DEPTH, REPO_MAP_MAX_FILES,
REPO_MAP_MAX_FILE_BYTES, REPO_MAP_MAX_VISIT,
};
use crate::gateway::SessionServiceError;
use agent_contracts::backend::{
capability::{filesystem::ReadBytesRequest, search::GlobRequest},
BackendPath, OperationBackend, OperationError, PathKind,
};
use std::path::{Path, PathBuf};
pub(crate) async fn compose_backend_workspace_section(
backend: &dyn OperationBackend,
workspace_root: &Path,
) -> Result<String, SessionServiceError> {
let mut sections = Vec::new();
let agents_path = workspace_root.join("AGENTS.md");
if let Some(content) = read_optional_backend_utf8(backend, &agents_path).await? {
let content = content.trim();
if !content.is_empty() {
let prompt_files = [WorkspacePromptFile {
path: agents_path,
content: content.to_string(),
}];
if let Some(block) = render_workspace_prompt_block(
&prompt_files,
"The following instructions were loaded from the runtime workspace root.",
) {
sections.push(block);
}
}
}
if let Some(repo_map) = compose_backend_repo_map(backend, workspace_root).await? {
sections.push(repo_map);
}
Ok(sections.join("\n\n"))
}
async fn compose_backend_repo_map(
backend: &dyn OperationBackend,
workspace_root: &Path,
) -> Result<Option<String>, SessionServiceError> {
let candidates = backend
.search()
.glob(GlobRequest {
pattern: "*".to_string(),
base_dir: Some(BackendPath(workspace_root.display().to_string())),
limit: Some(REPO_MAP_MAX_VISIT),
})
.await
.map_err(|error| SessionServiceError::RuntimeBuild {
message: format!("failed to enumerate backend workspace for repo map: {error}"),
})?;
let mut relative_paths = candidates
.into_iter()
.filter_map(|candidate| relative_backend_path(workspace_root, &candidate))
.filter(|relative| relative.components().count() <= REPO_MAP_MAX_DEPTH + 1)
.filter(|relative| {
!repo_map_path_is_skipped(relative) && repo_map_is_source(relative.as_path())
})
.collect::<Vec<_>>();
relative_paths.sort();
relative_paths.dedup();
let mut entries = Vec::new();
for relative_path in relative_paths {
if entries.len() >= REPO_MAP_MAX_FILES {
break;
}
let remote_path = workspace_root.join(&relative_path);
let stat = backend
.files()
.stat(&BackendPath(remote_path.display().to_string()))
.await
.map_err(|error| SessionServiceError::RuntimeBuild {
message: format!(
"failed to stat backend repo-map file {}: {error}",
remote_path.display()
),
})?;
if stat.kind != Some(PathKind::File) {
continue;
}
let signatures = if stat.size_bytes.unwrap_or(u64::MAX) <= REPO_MAP_MAX_FILE_BYTES {
read_backend_utf8(backend, &remote_path)
.await
.ok()
.map(|content| repo_map_signatures(&content))
.unwrap_or_default()
} else {
Vec::new()
};
entries.push(RepoMapEntry {
relative_path,
signatures,
});
}
Ok(render_repo_map(entries))
}
fn relative_backend_path(workspace_root: &Path, candidate: &BackendPath) -> Option<PathBuf> {
let candidate = Path::new(&candidate.0);
let relative = if candidate.is_absolute() {
candidate.strip_prefix(workspace_root).ok()?
} else {
candidate
};
(!relative.as_os_str().is_empty()).then(|| relative.to_path_buf())
}
pub(crate) async fn read_optional_backend_utf8(
backend: &dyn OperationBackend,
path: &Path,
) -> Result<Option<String>, SessionServiceError> {
match backend
.files()
.read_bytes(ReadBytesRequest {
path: BackendPath(path.display().to_string()),
})
.await
{
Ok(bytes) => {
String::from_utf8(bytes)
.map(Some)
.map_err(|error| SessionServiceError::RuntimeBuild {
message: format!("backend file is not UTF-8 {}: {error}", path.display()),
})
}
Err(OperationError::NotFound { .. }) => Ok(None),
Err(error) => Err(SessionServiceError::RuntimeBuild {
message: format!("failed to read backend file {}: {error}", path.display()),
}),
}
}
pub(crate) async fn read_backend_utf8(
backend: &dyn OperationBackend,
path: &Path,
) -> Result<String, SessionServiceError> {
read_optional_backend_utf8(backend, path)
.await?
.ok_or_else(|| SessionServiceError::RuntimeBuild {
message: format!("required backend file is missing: {}", path.display()),
})
}