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
54 changes: 25 additions & 29 deletions crates/ironclaw_agent_loop/src/executor/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1705,10 +1705,7 @@ mod tests {
use super::*;
use ironclaw_turns::{
LoopGateRef, LoopResultRef,
run_profile::{
CapabilityInputRef, CapabilityOutcome, CapabilitySurfaceVersion,
capability_outcome_to_resolution,
},
run_profile::{CapabilityInputRef, CapabilitySurfaceVersion, resolution},
};

fn call(input: &str) -> CapabilityCallCandidate {
Expand All @@ -1723,31 +1720,30 @@ mod tests {
}
}

// The fixtures build a loop-facing `CapabilityOutcome` and map it through the
// production mapping so `shared_await_dependent_gate` sees the exact
// `Resolution` the flip produces (origin preserved on the channel).
// The fixtures build the exact `Resolution` the producer constructors
// emit so `shared_await_dependent_gate` sees the flip's channel shape
// (origin preserved on the channel).
fn await_dependent(gate: &str, result: &str) -> Resolution {
capability_outcome_to_resolution(CapabilityOutcome::AwaitDependentRun {
gate_ref: LoopGateRef::new(gate).unwrap(),
result_ref: LoopResultRef::new(format!("result:{result}")).unwrap(),
safe_summary: "summary".to_string(),
byte_len: 0,
model_observation: None,
})
resolution::await_dependent_run(
LoopGateRef::new(gate).unwrap(),
LoopResultRef::new(format!("result:{result}")).unwrap(),
"summary".to_string(),
0,
None,
)
.resolution
}

fn completed(result: &str) -> Resolution {
capability_outcome_to_resolution(CapabilityOutcome::Completed(CapabilityResultMessage {
result_ref: LoopResultRef::new(format!("result:{result}")).unwrap(),
safe_summary: "summary".to_string(),
progress: CapabilityProgress::MadeProgress,
terminate_hint: false,
byte_len: 0,
output_digest: None,
model_observation: None,
}))
.resolution
resolution::completed(
LoopResultRef::new(format!("result:{result}")).unwrap(),
"summary".to_string(),
CapabilityProgress::MadeProgress,
false,
0,
None,
None,
)
}

#[test]
Expand Down Expand Up @@ -1788,11 +1784,11 @@ mod tests {
let calls = vec![call("a"), call("b")];
let outcomes = vec![
await_dependent("gate:1", "r1"),
capability_outcome_to_resolution(CapabilityOutcome::ApprovalRequired {
gate_ref: LoopGateRef::new("gate:approval").unwrap(),
safe_summary: "approval".to_string(),
approval_resume: None,
})
resolution::approval_required(
LoopGateRef::new("gate:approval").unwrap(),
"approval".to_string(),
None,
)
.resolution,
];
assert!(shared_await_dependent_gate(&calls, &outcomes).is_none());
Expand Down
903 changes: 174 additions & 729 deletions crates/ironclaw_agent_loop/src/executor/tests.rs

Large diffs are not rendered by default.

25 changes: 6 additions & 19 deletions crates/ironclaw_agent_loop/src/executor/tests/cancellation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{
resolution,
AgentLoopExecutor, AgentLoopExecutorError, AgentLoopHostError, AgentLoopHostErrorKind,
CanonicalAgentLoopExecutor, CapabilityFailureKind, CapabilityOutcome, CapabilityResultMessage,
CanonicalAgentLoopExecutor, CapabilityFailureKind,
CheckpointKind, HostStage, LoopCancelReasonKind, LoopCancelledReasonKind, LoopCheckpointKind,
LoopExecutionState, LoopExit, LoopGateRef, LoopInput, LoopInputAckToken, LoopInputBatch,
LoopInputCursor, LoopInterruptKind, LoopResultRef, LoopRunInfoPort, LoopSafeSummary, MockHost,
Expand Down Expand Up @@ -539,14 +540,8 @@ async fn cancellation_after_retry_prompt_rebuild_skips_second_model_call() {
#[tokio::test]
async fn capability_cancelled_returns_cancelled_exit_without_retry() {
let host = MockHost::new(vec![calls_response()]).with_batch_outcomes(vec![
ironclaw_turns::run_profile::CapabilityBatchOutcome {
outcomes: vec![CapabilityOutcome::Failed(
ironclaw_turns::run_profile::CapabilityFailure {
error_kind: CapabilityFailureKind::Cancelled,
safe_summary: "capability cancelled".to_string(),
detail: None,
},
)],
ironclaw_host_api::ResolutionBatch {
resolutions: vec![resolution::failed(CapabilityFailureKind::Cancelled, "capability cancelled".to_string(), None)],
stopped_on_suspension: false,
},
]);
Expand Down Expand Up @@ -670,16 +665,8 @@ async fn cancellation_after_before_side_effect_checkpoint_skips_capability_call(
async fn cancellation_after_capability_batch_preserves_completed_result() {
let result_ref = LoopResultRef::new("result:late-cancel").expect("valid");
let host = MockHost::new(vec![calls_response()])
.with_batch_outcomes(vec![ironclaw_turns::run_profile::CapabilityBatchOutcome {
outcomes: vec![CapabilityOutcome::Completed(CapabilityResultMessage {
result_ref: result_ref.clone(),
safe_summary: "completed before cancellation".to_string(),
progress: ironclaw_turns::run_profile::CapabilityProgress::MadeProgress,
terminate_hint: true,
byte_len: 0,
output_digest: None,
model_observation: None,
})],
.with_batch_outcomes(vec![ironclaw_host_api::ResolutionBatch {
resolutions: vec![resolution::completed(result_ref.clone(), "completed before cancellation".to_string(), ironclaw_turns::run_profile::CapabilityProgress::MadeProgress, true, 0, None, None)],
stopped_on_suspension: false,
}])
.cancel_after_batch_invocation();
Expand Down
61 changes: 17 additions & 44 deletions crates/ironclaw_agent_loop/src/executor/tests/failure_matrix.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{
resolution,
AgentLoopExecutor, AgentLoopExecutorError, AgentLoopHostError, AgentLoopHostErrorKind,
CanonicalAgentLoopExecutor, CapabilityFailureKind, CapabilityOutcome, CapabilityResultMessage,
CanonicalAgentLoopExecutor, CapabilityFailureKind,
CheckpointKind, DefaultCompactionStrategy, FixedReplyAdmissionPolicy, GateOutcome, HostStage,
LoopCheckpointKind, LoopCompactionError, LoopExecutionState, LoopExit, LoopFailureKind,
LoopGateRef, LoopResultRef, LoopSafeSummary, MockHost, active_task_preserving_compaction_index,
Expand Down Expand Up @@ -279,13 +280,7 @@ async fn run_setup(setup: FailureSetup) -> ObservedTerminal {
calls_response(),
reply_response_with_text("explanation"),
])
.with_batch_outcomes(vec![batch_outcome(CapabilityOutcome::Failed(
ironclaw_turns::run_profile::CapabilityFailure {
error_kind: CapabilityFailureKind::Permanent,
safe_summary: "permanent protocol failure".to_string(),
detail: None,
},
))]);
.with_batch_outcomes(vec![batch_outcome(resolution::failed(CapabilityFailureKind::Permanent, "permanent protocol failure".to_string(), None))]);
run_local(crate::families::default(), host, None).await
}
FailureSetup::CapabilityInvalidInputRecoverable => {
Expand Down Expand Up @@ -334,11 +329,7 @@ async fn run_setup(setup: FailureSetup) -> ObservedTerminal {
reply_response_with_text("completed"),
])
.with_batch_outcomes(vec![batch_outcome_stopped(
CapabilityOutcome::ApprovalRequired {
gate_ref: LoopGateRef::new("gate:approval-skip").expect("valid"),
safe_summary: "approval required".to_string(),
approval_resume: None,
},
resolution::approval_required(LoopGateRef::new("gate:approval-skip").expect("valid"), "approval required".to_string(), None).resolution,
)]);
run_local(
family_with_gate_outcome(GateOutcome::SkipAndContinue {
Expand All @@ -363,13 +354,7 @@ async fn run_setup(setup: FailureSetup) -> ObservedTerminal {
calls_response(),
reply_response_with_text("completed"),
])
.with_batch_outcomes(vec![batch_outcome(CapabilityOutcome::Denied(
ironclaw_turns::run_profile::CapabilityDenied {
reason_kind:
ironclaw_turns::run_profile::CapabilityDeniedReasonKind::EmptySurface,
safe_summary: "provider call denied".to_string(),
},
))]);
.with_batch_outcomes(vec![batch_outcome(resolution::denied(ironclaw_turns::run_profile::CapabilityDeniedReasonKind::EmptySurface, "provider call denied".to_string()).resolution)]);
run_local(crate::families::default(), host, None).await
}
FailureSetup::CapabilityPolicyDeniedRecoverable => {
Expand Down Expand Up @@ -580,39 +565,27 @@ fn assert_explanation_refs(row: &MatrixRow, refs: &[ironclaw_turns::LoopMessageR
}

fn batch_outcome(
outcome: CapabilityOutcome,
) -> ironclaw_turns::run_profile::CapabilityBatchOutcome {
ironclaw_turns::run_profile::CapabilityBatchOutcome {
outcomes: vec![outcome],
outcome: ironclaw_host_api::Resolution,
) -> ironclaw_host_api::ResolutionBatch {
ironclaw_host_api::ResolutionBatch {
resolutions: vec![outcome],
stopped_on_suspension: false,
}
}

fn failed_capability(error_kind: CapabilityFailureKind, safe_summary: &str) -> CapabilityOutcome {
CapabilityOutcome::Failed(ironclaw_turns::run_profile::CapabilityFailure {
error_kind,
safe_summary: safe_summary.to_string(),
detail: None,
})
fn failed_capability(error_kind: CapabilityFailureKind, safe_summary: &str) -> ironclaw_host_api::Resolution {
resolution::failed(error_kind, safe_summary.to_string(), None)
}

fn batch_outcome_stopped(
outcome: CapabilityOutcome,
) -> ironclaw_turns::run_profile::CapabilityBatchOutcome {
ironclaw_turns::run_profile::CapabilityBatchOutcome {
outcomes: vec![outcome],
outcome: ironclaw_host_api::Resolution,
) -> ironclaw_host_api::ResolutionBatch {
ironclaw_host_api::ResolutionBatch {
resolutions: vec![outcome],
stopped_on_suspension: true,
}
}

fn no_change_result(result_ref: &str) -> CapabilityOutcome {
CapabilityOutcome::Completed(CapabilityResultMessage {
result_ref: LoopResultRef::new(result_ref).expect("valid"),
safe_summary: "completed without progress".to_string(),
progress: ironclaw_turns::run_profile::CapabilityProgress::NoChange,
terminate_hint: false,
byte_len: 0,
output_digest: None,
model_observation: None,
})
fn no_change_result(result_ref: &str) -> ironclaw_host_api::Resolution {
resolution::completed(LoopResultRef::new(result_ref).expect("valid"), "completed without progress".to_string(), ironclaw_turns::run_profile::CapabilityProgress::NoChange, false, 0, None, None)
}
27 changes: 11 additions & 16 deletions crates/ironclaw_agent_loop/src/executor/tests/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use ironclaw_turns::{
run_profile::{
AgentLoopHostError, AgentLoopHostErrorKind, AppendCapabilityResultRef, AssistantReply,
CancellationPolicy, CapabilityBatchInvocation, CapabilityCallCandidate,
CapabilityDescriptorView, CapabilityInputRef, CapabilityInvocation, CapabilityOutcome,
CapabilityDescriptorView, CapabilityInputRef, CapabilityInvocation,
CapabilitySurfaceProfileId, CapabilitySurfaceVersion, CheckpointPolicy, CheckpointSchemaId,
ConcurrencyClass, ContextProfileId, FinalizeAssistantMessage, LoopCancelReasonKind,
LoopCancellationPort, LoopCancellationSignal, LoopCheckpointKind, LoopCheckpointRequest,
Expand All @@ -22,7 +22,7 @@ use ironclaw_turns::{
RegisterProviderToolCallRequest, ResolvedRunProfile, ResourceBudgetPolicy,
ResourceBudgetTier, RunClassId, RunProfileFingerprint, RuntimeProfileConstraints,
SchedulingClass, StageCheckpointPayloadRequest, SteeringPolicy, VisibleCapabilityRequest,
VisibleCapabilitySurface, capability_outcome_to_resolution,
VisibleCapabilitySurface,
},
};

Expand Down Expand Up @@ -53,8 +53,8 @@ pub(super) struct MockHost {
compaction: MockCompactionSupport,
input_batches: Arc<Mutex<VecDeque<LoopInputBatch>>>,
acked_input_tokens: Arc<Mutex<Vec<LoopInputAckToken>>>,
batch_outcomes: Arc<Mutex<VecDeque<ironclaw_turns::run_profile::CapabilityBatchOutcome>>>,
single_outcomes: Arc<Mutex<VecDeque<CapabilityOutcome>>>,
batch_outcomes: Arc<Mutex<VecDeque<ironclaw_host_api::ResolutionBatch>>>,
single_outcomes: Arc<Mutex<VecDeque<ironclaw_host_api::Resolution>>>,
checkpoints: Arc<Mutex<Vec<LoopCheckpointKind>>>,
batch_invocations: Arc<Mutex<Vec<CapabilityBatchInvocation>>>,
single_invocations: Arc<Mutex<Vec<CapabilityInvocation>>>,
Expand Down Expand Up @@ -167,13 +167,16 @@ impl MockHost {

pub(super) fn with_batch_outcomes(
self,
outcomes: Vec<ironclaw_turns::run_profile::CapabilityBatchOutcome>,
outcomes: Vec<ironclaw_host_api::ResolutionBatch>,
) -> Self {
*self.batch_outcomes.lock().expect("lock") = outcomes.into();
self
}

pub(super) fn with_single_outcomes(self, outcomes: Vec<CapabilityOutcome>) -> Self {
pub(super) fn with_single_outcomes(
self,
outcomes: Vec<ironclaw_host_api::Resolution>,
) -> Self {
*self.single_outcomes.lock().expect("lock") = outcomes.into();
self
}
Expand Down Expand Up @@ -775,7 +778,6 @@ impl ironclaw_turns::run_profile::LoopCapabilityPort for MockHost {
.ok_or_else(|| {
AgentLoopHostError::new(AgentLoopHostErrorKind::Internal, "single script exhausted")
})
.map(|outcome| capability_outcome_to_resolution(outcome).resolution)
}

async fn invoke_capability_batch(
Expand All @@ -786,7 +788,7 @@ impl ironclaw_turns::run_profile::LoopCapabilityPort for MockHost {
if let Some(kind) = *self.fail_batch_with.lock().expect("lock") {
return Err(AgentLoopHostError::new(kind, "scripted batch failure"));
}
let outcome = self
let batch = self
.batch_outcomes
.lock()
.expect("lock")
Expand All @@ -797,14 +799,7 @@ impl ironclaw_turns::run_profile::LoopCapabilityPort for MockHost {
if *self.cancel_after_batch_invocation.lock().expect("lock") {
self.request_cancellation(LoopCancelReasonKind::UserRequested);
}
Ok(ironclaw_host_api::ResolutionBatch {
resolutions: outcome
.outcomes
.into_iter()
.map(|o| capability_outcome_to_resolution(o).resolution)
.collect(),
stopped_on_suspension: outcome.stopped_on_suspension,
})
Ok(batch)
}
}

Expand Down
Loading
Loading