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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 25 additions & 24 deletions crates/ironclaw_agent_loop/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,7 @@ impl CanonicalAgentLoopExecutor {
CancelCheck::Exit(exit) => return Ok(exit),
};

let context_request = planner.context().plan_context_request(&state).await;
let prompt_mode = context_request.mode;
let surface_filter = planner.capability().filter(&state).await;
state = match self
.checkpoint_and_exit_if_cancelled_after_pending_input_ack(
host,
Expand All @@ -242,25 +241,14 @@ impl CanonicalAgentLoopExecutor {
CancelCheck::Continue(state) => *state,
CancelCheck::Exit(exit) => return Ok(exit),
};
let prompt_bundle = host
.build_prompt_bundle(context_request)
let mut surface = host
.visible_capabilities(VisibleCapabilityRequest)
.await
.map_err(|_| AgentLoopExecutorError::HostUnavailable {
stage: HostStage::Prompt,
stage: HostStage::Capability,
})?;
self.emit_progress(
host,
LoopProgressEvent::PromptBundleBuilt {
iteration: state.iteration,
bundle_ref: prompt_bundle.bundle_ref.clone(),
mode: prompt_mode,
surface_version: prompt_bundle.surface_version.clone(),
message_count: prompt_bundle.messages.len() as u32,
identity_message_count: prompt_bundle.identity_message_count,
instruction_snippet_count: prompt_bundle.instruction_snippet_count,
},
)
.await;
apply_capability_filter(&mut surface, &surface_filter);
state.surface_version = Some(surface.version.clone());
state = match self
.checkpoint_and_exit_if_cancelled_after_pending_input_ack(
host,
Expand All @@ -273,7 +261,9 @@ impl CanonicalAgentLoopExecutor {
CancelCheck::Exit(exit) => return Ok(exit),
};

let surface_filter = planner.capability().filter(&state).await;
let mut context_request = planner.context().plan_context_request(&state).await;
context_request.surface_version = Some(surface.version.clone());
let prompt_mode = context_request.mode;
state = match self
.checkpoint_and_exit_if_cancelled_after_pending_input_ack(
host,
Expand All @@ -285,14 +275,25 @@ impl CanonicalAgentLoopExecutor {
CancelCheck::Continue(state) => *state,
CancelCheck::Exit(exit) => return Ok(exit),
};
let mut surface = host
.visible_capabilities(VisibleCapabilityRequest)
let prompt_bundle = host
.build_prompt_bundle(context_request)
.await
.map_err(|_| AgentLoopExecutorError::HostUnavailable {
stage: HostStage::Capability,
stage: HostStage::Prompt,
})?;
apply_capability_filter(&mut surface, &surface_filter);
state.surface_version = Some(surface.version.clone());
self.emit_progress(
host,
LoopProgressEvent::PromptBundleBuilt {
iteration: state.iteration,
bundle_ref: prompt_bundle.bundle_ref.clone(),
mode: prompt_mode,
surface_version: prompt_bundle.surface_version.clone(),
message_count: prompt_bundle.messages.len() as u32,
identity_message_count: prompt_bundle.identity_message_count,
instruction_snippet_count: prompt_bundle.instruction_snippet_count,
},
)
.await;
state = match self
.checkpoint_and_exit_if_cancelled_after_pending_input_ack(
host,
Expand Down
2 changes: 1 addition & 1 deletion crates/ironclaw_agent_loop/tests/safety_nets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ async fn recovery_budget_exhaustion_uses_single_call_retry() {
calls.as_slice(),
[
MockHostCall::PollInputs,
MockHostCall::BuildPromptBundle,
MockHostCall::VisibleCapabilities,
MockHostCall::BuildPromptBundle,
MockHostCall::StageCheckpointPayload(CheckpointKind::BeforeModel),
MockHostCall::SaveCheckpoint(CheckpointKind::BeforeModel),
MockHostCall::StreamModel,
Expand Down
41 changes: 39 additions & 2 deletions crates/ironclaw_host_runtime/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ use ironclaw_turns::LibSqlTurnStateStore;
#[cfg(feature = "postgres")]
use ironclaw_turns::PostgresTurnStateStore;
use ironclaw_turns::{
DefaultTurnCoordinator, InMemoryTurnStateStore, NoopTurnRunWakeNotifier, TurnRunWakeNotifier,
TurnStateStore, runner::TurnRunTransitionPort,
DefaultTurnCoordinator, InMemoryTurnStateStore, NoopTurnRunWakeNotifier, RunProfileResolver,
TurnRunWakeNotifier, TurnStateStore, runner::TurnRunTransitionPort,
};
use ironclaw_wasm::{
DenyWasmHostHttp, EmptyWasmRuntimeCredentials, PreparedWitTool, WasmError,
Expand Down Expand Up @@ -169,6 +169,7 @@ pub enum ProductionWiringComponent {
WasmRuntime,
FirstPartyRuntime,
TurnState,
RunProfileResolver,
TurnRunWakeNotifier,
}

Expand All @@ -194,6 +195,7 @@ impl ProductionWiringComponent {
Self::WasmRuntime => "wasm_runtime",
Self::FirstPartyRuntime => "first_party_runtime",
Self::TurnState => "turn_state",
Self::RunProfileResolver => "run_profile_resolver",
Self::TurnRunWakeNotifier => "turn_run_wake_notifier",
}
}
Expand Down Expand Up @@ -275,6 +277,7 @@ struct ProductionComponentTypes {
mcp_runtime: Option<ProductionComponentType>,
first_party_runtime: Option<ProductionComponentType>,
turn_state: Option<ProductionComponentType>,
run_profile_resolver: Option<ProductionComponentType>,
turn_run_transition_port: Option<ProductionComponentType>,
turn_run_transition_port_verified: bool,
turn_run_wake_notifier: Option<ProductionComponentType>,
Expand Down Expand Up @@ -383,6 +386,7 @@ where
first_party_runtime: Option<Arc<FirstPartyCapabilityRegistry>>,
wasm_runtime: Option<Arc<WasmRuntimeAdapter>>,
turn_state: Option<Arc<dyn TurnStateStore>>,
run_profile_resolver: Option<Arc<dyn RunProfileResolver>>,
turn_run_transition_port: Option<Arc<dyn TurnRunTransitionPort>>,
turn_run_wake_notifier: Option<Arc<dyn TurnRunWakeNotifier>>,
component_types: ProductionComponentTypes,
Expand Down Expand Up @@ -438,6 +442,7 @@ where
first_party_runtime: None,
wasm_runtime: None,
turn_state: None,
run_profile_resolver: None,
turn_run_transition_port: None,
turn_run_wake_notifier: None,
component_types: ProductionComponentTypes {
Expand All @@ -462,6 +467,7 @@ where
mcp_runtime: None,
first_party_runtime: None,
turn_state: None,
run_profile_resolver: None,
turn_run_transition_port: None,
turn_run_transition_port_verified: false,
turn_run_wake_notifier: None,
Expand Down Expand Up @@ -501,6 +507,7 @@ where
first_party_runtime,
wasm_runtime,
turn_state,
run_profile_resolver,
turn_run_transition_port,
turn_run_wake_notifier,
mut component_types,
Expand Down Expand Up @@ -533,6 +540,7 @@ where
first_party_runtime,
wasm_runtime,
turn_state,
run_profile_resolver,
turn_run_transition_port,
turn_run_wake_notifier,
component_types,
Expand Down Expand Up @@ -587,6 +595,7 @@ where
first_party_runtime,
wasm_runtime,
turn_state,
run_profile_resolver,
turn_run_transition_port,
turn_run_wake_notifier,
mut component_types,
Expand Down Expand Up @@ -629,6 +638,7 @@ where
first_party_runtime,
wasm_runtime,
turn_state,
run_profile_resolver,
turn_run_transition_port,
turn_run_wake_notifier,
component_types,
Expand Down Expand Up @@ -819,6 +829,15 @@ where
self
}

pub fn with_run_profile_resolver<T>(mut self, resolver: Arc<T>) -> Self
where
T: RunProfileResolver + 'static,
{
self.component_types.run_profile_resolver = Some(ProductionComponentType::of::<T>());
self.run_profile_resolver = Some(resolver);
self
}

#[cfg(feature = "libsql")]
pub async fn with_libsql_turn_state_store(
self,
Expand Down Expand Up @@ -1119,6 +1138,11 @@ where
ProductionWiringComponent::TurnState,
self.turn_state.is_some(),
);
self.push_missing(
&mut issues,
ProductionWiringComponent::RunProfileResolver,
self.run_profile_resolver.is_some(),
);
self.push_missing(
&mut issues,
ProductionWiringComponent::TurnRunWakeNotifier,
Expand Down Expand Up @@ -1408,6 +1432,13 @@ where
None,
));
};
let Some(run_profile_resolver) = self.run_profile_resolver.as_ref() else {
return Err(production_wiring_report(
ProductionWiringComponent::RunProfileResolver,
ProductionWiringIssueKind::Missing,
None,
));
Comment on lines +1435 to +1440

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire a production run-profile resolver before coordinator build

turn_coordinator_for_production now hard-fails when run_profile_resolver is unset, but the production service assembly paths still never set one (crates/ironclaw_reborn_composition/src/factory.rs libsql path at lines 265-287 and postgres path at lines 327-349 only call .with_turn_run_wake_notifier(...) before turn_coordinator_for_production()). In production/migration profiles this now returns a missing-component wiring report and prevents coordinator construction, so bootstrapping the Reborn production facades fails even with valid DB/runtime wiring.

Useful? React with 👍 / 👎.

};
let Some(notifier) = self.turn_run_wake_notifier.as_ref() else {
return Err(production_wiring_report(
ProductionWiringComponent::TurnRunWakeNotifier,
Expand All @@ -1416,6 +1447,7 @@ where
));
};
Ok(DefaultTurnCoordinator::new(Arc::clone(turn_state))
.with_run_profile_resolver(Arc::clone(run_profile_resolver))
.with_wake_notifier(Arc::clone(notifier)))
}

Expand Down Expand Up @@ -1485,6 +1517,11 @@ where
ProductionWiringComponent::TurnState,
self.turn_state.is_some(),
);
self.push_missing(
&mut issues,
ProductionWiringComponent::RunProfileResolver,
self.run_profile_resolver.is_some(),
);
self.push_missing(
&mut issues,
ProductionWiringComponent::TurnRunWakeNotifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ use ironclaw_trust::{
use ironclaw_turns::LibSqlTurnStateStore;
#[cfg(feature = "libsql")]
use ironclaw_turns::{
AcceptedMessageRef, IdempotencyKey, ReplyTargetBindingRef, RunProfileRequest, SourceBindingRef,
SubmitTurnRequest, SubmitTurnResponse, TurnActor, TurnCoordinator, TurnScope, TurnStateStore,
AcceptedMessageRef, IdempotencyKey, InMemoryRunProfileResolver, ReplyTargetBindingRef,
RunProfileRequest, SourceBindingRef, SubmitTurnRequest, SubmitTurnResponse, TurnActor,
TurnCoordinator, TurnScope, TurnStateStore,
};
use ironclaw_turns::{NoopTurnRunWakeNotifier, TurnRunWake, TurnRunWakeNotifier};
use ironclaw_wasm::{
Expand Down Expand Up @@ -144,6 +145,13 @@ fn production_wiring_validation_rejects_missing_components_and_local_only_defaul
),
"missing turn-state store should be reported: {report:?}"
);
assert!(
report.contains(
ProductionWiringComponent::RunProfileResolver,
ProductionWiringIssueKind::Missing
),
"missing run-profile resolver should be reported: {report:?}"
);
assert!(
report.contains(
ProductionWiringComponent::TurnRunWakeNotifier,
Expand Down Expand Up @@ -749,6 +757,7 @@ async fn production_turn_coordinator_uses_configured_store_and_notifier() {
.with_libsql_turn_state_store(Arc::clone(&db))
.await
.unwrap()
.with_run_profile_resolver(Arc::new(InMemoryRunProfileResolver::default()))
.with_turn_run_wake_notifier(Arc::clone(&notifier));

let coordinator = services
Expand All @@ -771,6 +780,36 @@ async fn production_turn_coordinator_uses_configured_store_and_notifier() {
assert_eq!(notifier.wakes()[0].run_id, run_id);
}

#[cfg(feature = "libsql")]
#[tokio::test]
async fn production_turn_coordinator_requires_explicit_run_profile_resolver() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("turn-coordinator-missing-resolver.db");
let db = Arc::new(libsql::Builder::new_local(db_path).build().await.unwrap());

let services = HostRuntimeServices::new(
Arc::new(registry_with_manifest(SCRIPT_MANIFEST)),
Arc::new(LocalFilesystem::new()),
Arc::new(InMemoryResourceGovernor::new()),
Arc::new(GrantAuthorizer::new()),
ProcessServices::in_memory(),
CapabilitySurfaceVersion::new("surface-v1").unwrap(),
)
.with_libsql_turn_state_store(Arc::clone(&db))
.await
.unwrap()
.with_turn_run_wake_notifier(Arc::new(RecordingTurnRunWakeNotifier::default()));

let report = match services.turn_coordinator_for_production() {
Ok(_) => panic!("production turn coordinator must fail closed without a resolver"),
Err(report) => report,
};
assert!(report.contains(
ProductionWiringComponent::RunProfileResolver,
ProductionWiringIssueKind::Missing
));
}

#[test]
fn production_wiring_validation_rejects_noop_turn_wake_notifier() {
let services = HostRuntimeServices::new(
Expand Down
Loading
Loading