Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/decodex/src/agent/app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3702,8 +3702,9 @@ fn clear_thread_phase_goal_best_effort(
tracing::warn!(?error, "Failed to record app-server goal clear response.");
}
},
Err(error) =>
tracing::warn!(?error, "Failed to clear app-server phase goal after terminal path."),
Err(error) => {
tracing::warn!(?error, "Failed to clear app-server phase goal after terminal path.")
},
}
}

Expand Down
5 changes: 3 additions & 2 deletions apps/decodex/src/agent/tracker_tool_bridge/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1263,11 +1263,12 @@ impl<'a> TrackerToolBridge<'a> {
};
let details_json = match serde_json::to_string(&checkpoint_payload) {
Ok(details_json) => details_json,
Err(error) =>
Err(error) => {
return DynamicToolCallResponse::failure(format!(
"Failed to serialize the structured review checkpoint for issue `{}`: {error}",
self.issue.identifier
)),
));
},
};
let nonclean_rounds = match self.review_checkpoint_nonclean_rounds(
review_context,
Expand Down
10 changes: 6 additions & 4 deletions apps/decodex/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,10 +680,12 @@ impl ResearchCompileCommand {
self.source_issue.clone(),
self.outcome.into(),
)),
(None, None) =>
eyre::bail!("research compile requires either --input <JSON> or --intent <TEXT>."),
(Some(_), Some(_)) =>
eyre::bail!("research compile accepts --input or --intent, not both."),
(None, None) => {
eyre::bail!("research compile requires either --input <JSON> or --intent <TEXT>.")
},
(Some(_), Some(_)) => {
eyre::bail!("research compile accepts --input or --intent, not both.")
},
}
}
}
Expand Down
92 changes: 92 additions & 0 deletions apps/decodex/src/orchestrator/agent_evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ struct PrivateEvidenceReadback {
latest_event_type: Option<String>,
latest_event_at: Option<String>,
decision_requests: Vec<PrivateEvidenceDecisionRequestSummary>,
architecture_recoveries: Vec<PrivateEvidenceArchitectureRecoverySummary>,
improvement_candidates: Vec<HarnessImprovementCandidateSummary>,
events: Vec<PrivateEvidenceReadbackEvent>,
warnings: Vec<String>,
Expand All @@ -276,6 +277,16 @@ struct PrivateEvidenceDecisionRequestSummary {
resume_condition: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
struct PrivateEvidenceArchitectureRecoverySummary {
reason_code: String,
guardrail_reason: Option<String>,
boundary_disposition: Option<String>,
recovery_budget_attempt: Option<u64>,
recovery_budget_max_attempts: Option<u64>,
next_action: String,
}

#[derive(Clone, Debug, PartialEq, Serialize)]
struct PrivateEvidenceReadbackEvent {
record_id: i64,
Expand Down Expand Up @@ -660,6 +671,7 @@ fn build_private_evidence_readback(
latest_event_type: latest_event.map(|event| event.event_type().to_owned()),
latest_event_at: latest_event.map(|event| event.recorded_at().to_owned()),
decision_requests: authority_decision_requests_from_private_events(&events),
architecture_recoveries: architecture_recoveries_from_private_events(&events),
improvement_candidates: harness_improvement_candidates_from_private_events(&events),
events: events
.iter()
Expand Down Expand Up @@ -802,6 +814,86 @@ fn authority_decision_request_from_private_event(
})
}

fn architecture_recoveries_from_private_events(
events: &[state::PrivateExecutionEvent],
) -> Vec<PrivateEvidenceArchitectureRecoverySummary> {
events
.iter()
.filter(|event| {
matches!(
event.event_type(),
ARCHITECTURE_RECOVERY_PACKET_EVENT_TYPE
| ARCHITECTURE_RECOVERY_STARTED_EVENT_TYPE
| ARCHITECTURE_RECOVERY_TERMINAL_EVENT_TYPE
)
})
.filter_map(architecture_recovery_from_private_event)
.collect()
}

fn architecture_recovery_from_private_event(
event: &state::PrivateExecutionEvent,
) -> Option<PrivateEvidenceArchitectureRecoverySummary> {
let payload = event.payload();
let reason_code = payload.get("reason_code")?.as_str()?.to_owned();
let guardrail_reason = payload
.get("guardrail_reason")
.and_then(Value::as_str)
.or_else(|| {
payload
.get("loop_guardrail")
.and_then(|guardrail| guardrail.get("reason"))
.and_then(Value::as_str)
})
.map(str::to_owned);
let boundary_disposition = payload
.get("boundary_disposition")
.and_then(Value::as_str)
.or_else(|| {
payload
.get("authority_boundary_check")
.and_then(|boundary| boundary.get("disposition"))
.and_then(Value::as_str)
})
.map(str::to_owned);
let recovery_budget_attempt = payload
.get("recovery_budget")
.and_then(|budget| budget.get("attempt"))
.and_then(Value::as_u64);
let recovery_budget_max_attempts = payload
.get("recovery_budget")
.and_then(|budget| budget.get("max_attempts"))
.and_then(Value::as_u64);
let next_action = architecture_recovery_next_action(&reason_code);

Some(PrivateEvidenceArchitectureRecoverySummary {
reason_code,
guardrail_reason,
boundary_disposition,
recovery_budget_attempt,
recovery_budget_max_attempts,
next_action,
})
}

fn architecture_recovery_next_action(reason_code: &str) -> String {
match reason_code {
"architecture_recovery_started" => {
String::from("Retry with a materially different implementation strategy inside authority.")
},
"architecture_recovery_exhausted" => {
String::from("Require a new accepted recovery strategy or architecture decision before retrying.")
},
"external_dependency_required" => {
String::from("Resolve the dependency or Execution Program readiness blocker before retrying.")
},
"contract_boundary_required" => {
String::from("Resolve the Decision Contract or Authority Envelope boundary before retrying.")
},
_ => String::from("Inspect the Architecture Recovery Packet before retrying."),
}
}

fn private_evidence_run_matches_issue(
project: &ServiceConfig,
run: &ProjectRunStatus,
Expand Down
29 changes: 28 additions & 1 deletion apps/decodex/src/orchestrator/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1103,8 +1103,10 @@ where
} else {
let retry_budget_attempts =
child_exit_retry_budget_attempt_count(&context, &issue, child)?;
let retry_budget_limit =
child_exit_retry_budget_limit(&context, &issue, child)?;

if retry_budget_attempts >= context.workflow.frontmatter().execution().max_attempts() {
if retry_budget_attempts >= retry_budget_limit {
return terminalize_exhausted_child_exit_retry(
context,
issue,
Expand Down Expand Up @@ -1227,6 +1229,31 @@ where
Ok(u32::try_from(retry_budget_attempts).unwrap_or(u32::MAX).max(1))
}

fn child_exit_retry_budget_limit<T>(
context: &ChildExitRetryContext<'_, T>,
issue: &TrackerIssue,
child: ChildRunRef<'_>,
) -> Result<u32>
where
T: IssueTracker,
{
let max_attempts = context.workflow.frontmatter().execution().max_attempts();
let worktree = child_exit_worktree_spec(context, issue)?;
let Some(marker) = state::read_run_activity_marker_snapshot(&worktree.path)? else {
return Ok(max_attempts);
};

if marker.run_id() == child.run_id
&& marker.attempt_number() == child.attempt_number
&& marker.retry_kind() == Some(ARCHITECTURE_RECOVERY_RETRY_KIND)
{
return Ok(max_attempts
.saturating_add(u32::try_from(ARCHITECTURE_RECOVERY_BUDGET).unwrap_or(0)));
}

Ok(max_attempts)
}

fn terminalize_exhausted_child_exit_retry<T>(
context: ChildExitRetryContext<'_, T>,
issue: TrackerIssue,
Expand Down
Loading