Skip to content

Commit d7f1fd5

Browse files
authored
Add engine v2 action discovery metadata (#2876)
* Add engine v2 action discovery metadata * fix(engine): address action discovery review (#2876) * fix(engine): address follow-up review comments (#2876) * fix(engine): satisfy clippy in orchestrator lookup * fix(engine): propagate action snapshots in executor paths (#2876) * fix(bridge): restrict tool_info to callable actions (#2876) * [codex] Finish engine v2 deferred action inventory cleanup (#2889) * Add deferred action inventory groundwork * fix(engine): address deferred action inventory follow-up * fix(engine): address deferred inventory review feedback * test: fix fmt and clippy failures
1 parent ba48866 commit d7f1fd5

38 files changed

Lines changed: 2198 additions & 367 deletions

crates/ironclaw_engine/src/capability/planner.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ mod tests {
9797
parameters_schema: serde_json::json!({}),
9898
effects,
9999
requires_approval,
100+
discovery: None,
100101
}
101102
}
102103

crates/ironclaw_engine/src/capability/policy.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ mod tests {
215215
parameters_schema: serde_json::json!({}),
216216
effects,
217217
requires_approval,
218+
discovery: None,
218219
}
219220
}
220221

crates/ironclaw_engine/src/capability/registry.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,15 @@ mod tests {
8989
parameters_schema: serde_json::json!({"type": "object"}),
9090
effects: vec![EffectType::WriteExternal, EffectType::CredentialedNetwork],
9191
requires_approval: false,
92+
discovery: None,
9293
},
9394
ActionDef {
9495
name: "list_prs".into(),
9596
description: "List pull requests".into(),
9697
parameters_schema: serde_json::json!({"type": "object"}),
9798
effects: vec![EffectType::ReadExternal, EffectType::CredentialedNetwork],
9899
requires_approval: false,
100+
discovery: None,
99101
},
100102
],
101103
knowledge: vec!["When creating issues, always add labels.".into()],
@@ -144,6 +146,7 @@ mod tests {
144146
parameters_schema: serde_json::json!({"type": "object"}),
145147
effects: vec![EffectType::ReadLocal],
146148
requires_approval: false,
149+
discovery: None,
147150
}],
148151
knowledge: vec![],
149152
policies: vec![],

crates/ironclaw_engine/src/executor/context.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ mod tests {
182182
source_channel: None,
183183
user_timezone: None,
184184
thread_goal: Some("search the web".into()),
185+
available_actions_snapshot: None,
186+
available_action_inventory_snapshot: None,
185187
},
186188
)
187189
.await
@@ -220,6 +222,8 @@ mod tests {
220222
source_channel: None,
221223
user_timezone: None,
222224
thread_goal: Some("hello".into()),
225+
available_actions_snapshot: None,
226+
available_action_inventory_snapshot: None,
223227
},
224228
)
225229
.await
@@ -254,6 +258,8 @@ mod tests {
254258
source_channel: None,
255259
user_timezone: None,
256260
thread_goal: Some("hello".into()),
261+
available_actions_snapshot: None,
262+
available_action_inventory_snapshot: None,
257263
},
258264
)
259265
.await

crates/ironclaw_engine/src/executor/loop_engine.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,20 @@ impl ExecutionLoop {
266266
Vec::new()
267267
}
268268
};
269+
let inventory = match self
270+
.effects
271+
.available_action_inventory(&active_leases, &prompt_context)
272+
.await
273+
{
274+
Ok(inventory) => inventory,
275+
Err(error) => {
276+
debug!(
277+
thread_id = %self.thread.id,
278+
"failed to load action inventory for system prompt refresh: {error}"
279+
);
280+
crate::types::capability::ActionInventory::default()
281+
}
282+
};
269283

270284
if (!system_docs_loaded || !capabilities_loaded)
271285
&& self.has_engine_owned_system_prompt(checkpoint)
@@ -278,9 +292,8 @@ impl ExecutionLoop {
278292
);
279293
return;
280294
}
281-
282295
let system_prompt = crate::executor::prompt::build_codeact_system_prompt_with_docs(
283-
&[],
296+
&inventory,
284297
&capabilities,
285298
system_docs,
286299
self.platform_info.as_ref(),
@@ -692,6 +705,7 @@ mod tests {
692705
parameters_schema: serde_json::json!({"type": "object"}),
693706
effects: vec![EffectType::ReadLocal],
694707
requires_approval: false,
708+
discovery: None,
695709
}
696710
}
697711

@@ -1791,6 +1805,7 @@ mod tests {
17911805
parameters_schema: serde_json::json!({"type": "object"}),
17921806
effects: vec![EffectType::WriteExternal],
17931807
requires_approval: false,
1808+
discovery: None,
17941809
};
17951810

17961811
let llm = Arc::new(MockLlm::new(vec![

crates/ironclaw_engine/src/executor/orchestrator.rs

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,18 @@ async fn handle_execute_action(
980980

981981
let call_id = extract_string_kwarg(kwargs, "call_id").unwrap_or_default();
982982

983-
let exec_ctx = thread_execution_context(thread, StepId::new(), Some(call_id.clone()));
983+
let mut exec_ctx = thread_execution_context(thread, StepId::new(), Some(call_id.clone()));
984+
let active_leases = leases.active_for_thread(thread.id).await;
985+
let inventory = Arc::new(
986+
effects
987+
.available_action_inventory(&active_leases, &exec_ctx)
988+
.await
989+
.unwrap_or_default(),
990+
);
991+
let available_actions: Arc<[crate::types::capability::ActionDef]> =
992+
inventory.inline.clone().into();
993+
exec_ctx.available_actions_snapshot = Some(Arc::clone(&available_actions));
994+
exec_ctx.available_action_inventory_snapshot = Some(Arc::clone(&inventory));
984995

985996
// Helper: emit event only. The orchestrator owns transcript recording.
986997
let emit_and_record = |thread: &mut Thread,
@@ -1027,13 +1038,14 @@ async fn handle_execute_action(
10271038
};
10281039

10291040
// 2. Check policy
1030-
let action_def = effects
1031-
.available_actions(std::slice::from_ref(&lease), &exec_ctx)
1032-
.await
1033-
.ok()
1034-
.and_then(|actions| actions.into_iter().find(|a| a.name == name));
1041+
let action_def = available_actions.iter().find(|a| a.matches_name(&name));
10351042

1036-
if let Some(ref ad) = action_def {
1043+
let canonical_name = action_def
1044+
.as_ref()
1045+
.map(|action| action.name.clone())
1046+
.unwrap_or_else(|| name.clone());
1047+
1048+
if let Some(ad) = action_def {
10371049
match policy.evaluate(ad, &lease, &[]) {
10381050
crate::capability::policy::PolicyDecision::Deny { reason } => {
10391051
let output = serde_json::json!({"error": format!("Denied: {reason}")});
@@ -1129,10 +1141,10 @@ async fn handle_execute_action(
11291141
};
11301142

11311143
// 4. Execute
1132-
let ps = summarize_params(&name, &params);
1144+
let ps = summarize_params(&canonical_name, &params);
11331145
let execution_start = std::time::Instant::now();
11341146
match effects
1135-
.execute_action(&name, params, &lease, &exec_ctx)
1147+
.execute_action(&canonical_name, params, &lease, &exec_ctx)
11361148
.await
11371149
{
11381150
Ok(r) => {
@@ -1323,6 +1335,16 @@ async fn handle_execute_actions_parallel(
13231335
}
13241336

13251337
let step_id = StepId::new();
1338+
let actions_context = thread_execution_context(thread, step_id, None);
1339+
let active_leases = leases.active_for_thread(thread.id).await;
1340+
let inventory = Arc::new(
1341+
effects
1342+
.available_action_inventory(&active_leases, &actions_context)
1343+
.await
1344+
.unwrap_or_default(),
1345+
);
1346+
let available_actions: Arc<[crate::types::capability::ActionDef]> =
1347+
inventory.inline.clone().into();
13261348

13271349
// ── Phase 1: Preflight (sequential) ─────────────────────────
13281350
// Check leases and policies. Denied → error result. Approval → interrupt.
@@ -1369,12 +1391,16 @@ async fn handle_execute_actions_parallel(
13691391
};
13701392

13711393
// Check policy
1372-
let exec_ctx = thread_execution_context(thread, step_id, Some(pc.call_id.clone()));
1373-
let action_def = effects
1374-
.available_actions(std::slice::from_ref(&lease), &exec_ctx)
1375-
.await
1376-
.ok()
1377-
.and_then(|actions| actions.into_iter().find(|a| a.name == pc.name));
1394+
let mut exec_ctx = thread_execution_context(thread, step_id, Some(pc.call_id.clone()));
1395+
exec_ctx.available_actions_snapshot = Some(Arc::clone(&available_actions));
1396+
let action_def = available_actions
1397+
.iter()
1398+
.find(|a| a.matches_name(&pc.name))
1399+
.cloned();
1400+
let action_name = action_def
1401+
.as_ref()
1402+
.map(|action| action.name.clone())
1403+
.unwrap_or_else(|| pc.name.clone());
13781404

13791405
if let Some(ref ad) = action_def {
13801406
match policy.evaluate(ad, &lease, &[]) {
@@ -1386,7 +1412,7 @@ async fn handle_execute_actions_parallel(
13861412
});
13871413
let event = EventKind::ActionFailed {
13881414
step_id,
1389-
action_name: pc.name.clone(),
1415+
action_name: action_name.clone(),
13901416
call_id: pc.call_id.clone(),
13911417
error: reason,
13921418
duration_ms: 0,
@@ -1505,7 +1531,6 @@ async fn handle_execute_actions_parallel(
15051531
let mut slot_results: Vec<Option<serde_json::Value>> = vec![None; parsed.len()];
15061532
let mut slot_events: Vec<Option<EventKind>> = vec![None; parsed.len()];
15071533
let mut slot_outputs: Vec<Option<serde_json::Value>> = vec![None; parsed.len()];
1508-
15091534
// Separate runnable from errors
15101535
let mut runnable: Vec<(usize, crate::types::capability::CapabilityLease)> = Vec::new();
15111536
for (idx, pf) in preflight.into_iter().enumerate() {
@@ -1530,11 +1555,18 @@ async fn handle_execute_actions_parallel(
15301555
// Single call: execute directly
15311556
let (idx, lease) = runnable.into_iter().next().unwrap(); // safety: len()==1 checked above
15321557
let pc = &parsed[idx];
1533-
let exec_ctx = thread_execution_context(thread, step_id, Some(pc.call_id.clone()));
1534-
let ps = summarize_params(&pc.name, &pc.params);
1558+
let action_name = available_actions
1559+
.iter()
1560+
.find(|action| action.matches_name(&pc.name))
1561+
.map(|action| action.name.clone())
1562+
.unwrap_or_else(|| pc.name.clone());
1563+
let mut exec_ctx = thread_execution_context(thread, step_id, Some(pc.call_id.clone()));
1564+
exec_ctx.available_actions_snapshot = Some(Arc::clone(&available_actions));
1565+
exec_ctx.available_action_inventory_snapshot = Some(Arc::clone(&inventory));
1566+
let ps = summarize_params(&action_name, &pc.params);
15351567
let (result_json, event, output) = execute_single_action(
15361568
effects,
1537-
&pc.name,
1569+
&action_name,
15381570
pc.params.clone(),
15391571
&pc.call_id,
15401572
&lease,
@@ -1555,12 +1587,18 @@ async fn handle_execute_actions_parallel(
15551587
// Capture once outside the loop — the thread's metadata is stable
15561588
// for the duration of the parallel batch.
15571589
for (idx, lease) in runnable {
1558-
let pc_name = parsed[idx].name.clone();
1590+
let pc_name = available_actions
1591+
.iter()
1592+
.find(|action| action.matches_name(&parsed[idx].name))
1593+
.map(|action| action.name.clone())
1594+
.unwrap_or_else(|| parsed[idx].name.clone());
15591595
let pc_params = parsed[idx].params.clone();
15601596
let pc_call_id = parsed[idx].call_id.clone();
15611597
let effects = effects.clone();
15621598
let lease = lease.clone();
1563-
let exec_ctx = thread_execution_context(thread, step_id, Some(pc_call_id.clone()));
1599+
let mut exec_ctx = thread_execution_context(thread, step_id, Some(pc_call_id.clone()));
1600+
exec_ctx.available_actions_snapshot = Some(Arc::clone(&available_actions));
1601+
exec_ctx.available_action_inventory_snapshot = Some(Arc::clone(&inventory));
15641602
let ps = summarize_params(&pc_name, &pc_params);
15651603

15661604
join_set.spawn(async move {

0 commit comments

Comments
 (0)