use super::*;
pub(super) fn subagent_failure_message(
error: &anyhow::Error,
retryable: bool,
provider_retry_attempt: u64,
) -> String {
let error = error.downcast_ref::<HookPolicyError>().map_or_else(
|| error.to_string(),
|hook_error| hook_error.parent_safe_summary().to_string(),
);
if retryable && provider_retry_attempt >= SUBAGENT_PROVIDER_RETRY_MAX_ATTEMPTS {
format!(
"[provider retry exhausted after {} attempts] {error}",
provider_retry_attempt.saturating_add(1)
)
} else {
error
}
}
pub(super) fn completed_subagent_result(
id: String,
task: SubagentTask,
cwd: PathBuf,
session: Option<&crate::sessions::Session>,
output: AgentRunOutput,
structured_output: Option<Value>,
usage: Option<crate::output::NormalizedUsageAggregate>,
) -> SubagentTaskResult {
let mut output_text = output.text;
let output_truncated =
truncate_string_field(&mut output_text, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
SubagentTaskResult {
id,
status: SubagentStatus::Completed,
intent: task.intent,
agent: task.agent,
identity: task.identity,
cwd,
session_id: session.map(|session| session.id().to_string()),
session_path: session.map(|session| session.path().to_path_buf()),
total_tokens: output.total_tokens,
usage,
changed_files: Vec::new(),
output: output_text,
structured_output,
output_truncated,
error: None,
}
}
pub(in crate::subagents) struct FailedSubagentInput<'a> {
pub(super) config: &'a SubagentRunConfig,
pub(super) task_activity_id: ActivityId,
pub(super) id: String,
pub(super) task: SubagentTask,
pub(super) cwd: PathBuf,
pub(super) error: String,
pub(super) finisher: &'a TaskActivityFinisher,
pub(super) session: Option<&'a crate::sessions::Session>,
pub(super) partial_output: Option<BoundedPartialOutput>,
pub(super) usage: Option<crate::output::NormalizedUsageAggregate>,
}
pub(in crate::subagents) fn finish_failed_subagent(
input: FailedSubagentInput<'_>,
) -> SubagentTaskResult {
let FailedSubagentInput {
config,
task_activity_id,
id,
task,
cwd,
error,
finisher,
session,
partial_output,
usage,
} = input;
let partial_output = partial_output.filter(|output| !output.text.trim().is_empty());
let output_truncated = partial_output
.as_ref()
.is_some_and(|output| output.truncated);
let output = partial_output
.map(|output| output.text)
.or_else(|| failed_subagent_session_snapshot(session));
let mut result = failed_result_with_session_and_output(
id,
task,
cwd,
session.map(|session| session.id().to_string()),
session.map(|session| session.path().to_path_buf()),
error,
output,
);
result.usage = usage;
result.output_truncated |= output_truncated;
finisher.finish(config, task_activity_id, ActivityStatus::Failed);
result
}
pub(in crate::subagents) fn failed_result_with_session(
id: String,
task: SubagentTask,
cwd: PathBuf,
session_id: Option<String>,
session_path: Option<PathBuf>,
error: String,
) -> SubagentTaskResult {
failed_result_with_session_and_usage(id, task, cwd, session_id, session_path, error, None)
}
pub(in crate::subagents) fn failed_result_with_session_and_usage(
id: String,
task: SubagentTask,
cwd: PathBuf,
session_id: Option<String>,
session_path: Option<PathBuf>,
error: String,
usage: Option<crate::output::NormalizedUsageAggregate>,
) -> SubagentTaskResult {
failed_result_with_session_and_output_and_usage(
id,
task,
cwd,
session_id,
session_path,
error,
None,
usage,
)
}
pub(in crate::subagents) fn failed_result_with_session_and_output(
id: String,
task: SubagentTask,
cwd: PathBuf,
session_id: Option<String>,
session_path: Option<PathBuf>,
error: String,
output: Option<String>,
) -> SubagentTaskResult {
failed_result_with_session_and_output_and_usage(
id,
task,
cwd,
session_id,
session_path,
error,
output,
None,
)
}
#[expect(clippy::too_many_arguments)]
fn failed_result_with_session_and_output_and_usage(
id: String,
task: SubagentTask,
cwd: PathBuf,
session_id: Option<String>,
session_path: Option<PathBuf>,
error: String,
output: Option<String>,
usage: Option<crate::output::NormalizedUsageAggregate>,
) -> SubagentTaskResult {
let mut output = output.unwrap_or_default();
let output_was_truncated =
truncate_string_field(&mut output, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
SubagentTaskResult {
id,
status: SubagentStatus::Failed,
intent: task.intent,
agent: task.agent,
identity: task.identity,
cwd,
session_id,
session_path,
total_tokens: None,
usage,
changed_files: Vec::new(),
output,
structured_output: None,
output_truncated: output_was_truncated,
error: Some(redact_sensitive_text(&error)),
}
}
pub(in crate::subagents) fn format_duration(duration: Duration) -> String {
format!("{:.3}s", duration.as_secs_f64())
}
pub(in crate::subagents) fn failed_subagent_session_snapshot(
session: Option<&crate::sessions::Session>,
) -> Option<String> {
let session = session?;
let events = session
.read_recent_events_tolerant(SNAPSHOT_EVENT_LIMIT, SNAPSHOT_BYTE_LIMIT)
.ok()?
.events;
let mut assistant_chunks = String::new();
let mut authoritative_assistant_output = None;
let mut recovery = Vec::new();
for event in events {
match event.kind() {
Some(SessionEventKind::AssistantChunk) => {
if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
assistant_chunks.push_str(text);
}
}
Some(SessionEventKind::AssistantOutput) => {
if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
authoritative_assistant_output = Some(text.to_string());
}
}
Some(SessionEventKind::ReasoningSummary) => {
if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
recovery.push(format!("reasoning summary: {text}"));
}
}
Some(SessionEventKind::AbortRecovery) => {
recovery.push(format!("abort recovery: {}", event.payload));
}
Some(SessionEventKind::TurnStatus) => {
if let Some(payload) = event.turn_status_payload()
&& matches!(
payload.status,
crate::sessions::TurnStatus::Cancelled
| crate::sessions::TurnStatus::Failed
| crate::sessions::TurnStatus::CompactionRequired
)
&& let Some(text) = payload.assistant_text
{
authoritative_assistant_output = Some(text);
}
}
_ => {}
}
}
let assistant = authoritative_assistant_output
.filter(|text| !text.trim().is_empty())
.unwrap_or(assistant_chunks);
let mut snapshot = String::new();
if !assistant.trim().is_empty() {
snapshot.push_str("Partial subagent output before failure:\n");
snapshot.push_str(assistant.trim());
snapshot.push('\n');
}
for item in recovery {
if !snapshot.is_empty() {
snapshot.push('\n');
}
snapshot.push_str(&item);
}
if snapshot.trim().is_empty() {
None
} else {
truncate_string_field(&mut snapshot, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
Some(snapshot)
}
}
pub(in crate::subagents) fn truncate_subagents_output(output: &mut SubagentsOutput) {
for result in &mut output.results {
let output_was_truncated =
truncate_string_field(&mut result.output, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
let error_was_truncated = result
.error
.as_mut()
.is_some_and(|error| truncate_string_field(error, SUBAGENT_RESULT_ERROR_CHAR_LIMIT));
result.output_truncated |= output_was_truncated || error_was_truncated;
}
}
pub(in crate::subagents) fn truncate_string_field(value: &mut String, char_limit: usize) -> bool {
let char_count = value.chars().count();
if char_count <= char_limit {
return false;
}
if char_limit == 0 {
value.clear();
return true;
}
let marker = format!("{SUBAGENT_TRUNCATION_MARKER}{char_count} limit={char_limit}]");
let marker_len = marker.chars().count();
if marker_len >= char_limit {
*value = marker.chars().take(char_limit).collect();
return true;
}
let keep = char_limit - marker_len;
let mut truncated = value.chars().take(keep).collect::<String>();
truncated.push_str(&marker);
*value = truncated;
true
}