Skip to content

Commit 43b77a0

Browse files
committed
feat: Intelligent execution logic (Chain of Thought, Evaluator Model, State Projection Cache, File-based WASM Hotloading)
1 parent 15320f2 commit 43b77a0

13 files changed

Lines changed: 362 additions & 91 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rain-engine-cli/examples/customer_support_agent.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ fn main() {
2828
store: StoreBootstrapConfig::Postgres {
2929
database_url: "postgres://postgres:postgres@localhost/rain_engine".to_string(),
3030
},
31+
cache: None,
3132
blob: BlobBootstrapConfig::LocalDirectory {
3233
path: "./.rain-engine/blobs".to_string(),
3334
},

rain-engine-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ tokio-util.workspace = true
1616
tracing.workspace = true
1717
uuid.workspace = true
1818
typeshare.workspace = true
19+
redis = { workspace = true, features = ["tokio-comp"] }

rain-engine-core/src/engine.rs

Lines changed: 114 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ impl RegisteredSkill {
165165
pub struct AgentEngine {
166166
llm: Arc<dyn LlmProvider>,
167167
memory: Arc<dyn MemoryStore>,
168+
state_cache: Arc<dyn crate::StateProjectionCache>,
168169
skill_store: Option<Arc<dyn SkillStore>>,
169170
skills: Arc<DashMap<String, RegisteredSkill>>,
170171
planner: Option<Arc<dyn Planner>>,
@@ -175,12 +176,22 @@ impl AgentEngine {
175176
Self {
176177
llm,
177178
memory,
179+
state_cache: Arc::new(crate::InMemoryStateCache::new()),
178180
skill_store: None,
179181
skills: Arc::new(DashMap::new()),
180182
planner: None,
181183
}
182184
}
183185

186+
pub fn state_cache(&self) -> Arc<dyn crate::StateProjectionCache> {
187+
self.state_cache.clone()
188+
}
189+
190+
pub fn with_state_cache(mut self, state_cache: Arc<dyn crate::StateProjectionCache>) -> Self {
191+
self.state_cache = state_cache;
192+
self
193+
}
194+
184195
pub fn with_skill_store(mut self, skill_store: Arc<dyn SkillStore>) -> Self {
185196
self.skill_store = Some(skill_store);
186197
self
@@ -269,7 +280,7 @@ impl AgentEngine {
269280
trigger: request.trigger.clone(),
270281
intent: None,
271282
};
272-
if let Err(err) = self.memory.append_trigger(trigger_record).await {
283+
if let Err(err) = self.memory.append_trigger(trigger_record.clone()).await {
273284
return Ok(AdvanceResult {
274285
outcome: Some(storage_failure_outcome(trigger_id, 0, err.message)),
275286
emitted_events: Vec::new(),
@@ -278,15 +289,21 @@ impl AgentEngine {
278289
});
279290
}
280291

281-
let mut snapshot = match self.memory.load_session(&request.session_id).await {
282-
Ok(snapshot) => snapshot,
283-
Err(err) => {
284-
return Ok(AdvanceResult {
285-
outcome: Some(storage_failure_outcome(trigger_id, 0, err.message)),
286-
emitted_events: Vec::new(),
287-
state_delta: AgentStateDelta::default(),
288-
wake_request: None,
289-
});
292+
let mut snapshot = match self.state_cache.get_projection(&request.session_id).await {
293+
Ok(Some(mut cached)) => {
294+
cached.records.push(SessionRecord::Trigger(trigger_record));
295+
cached
296+
}
297+
_ => match self.memory.load_session(&request.session_id).await {
298+
Ok(snapshot) => snapshot,
299+
Err(err) => {
300+
return Ok(AdvanceResult {
301+
outcome: Some(storage_failure_outcome(trigger_id, 0, err.message)),
302+
emitted_events: Vec::new(),
303+
state_delta: AgentStateDelta::default(),
304+
wake_request: None,
305+
});
306+
}
290307
}
291308
};
292309
counter!("rain_engine.triggers_total").increment(1);
@@ -681,7 +698,7 @@ impl AgentEngine {
681698
return Ok(build_advance_result(outcome, emitted_events));
682699
}
683700

684-
let available_skills = self
701+
let mut available_skills = self
685702
.skills
686703
.iter()
687704
.filter(|skill| {
@@ -695,13 +712,38 @@ impl AgentEngine {
695712
.map(|skill| skill.value().definition())
696713
.collect::<Vec<_>>();
697714

715+
// Enforce Strategy Preferences: If a tool has a high failure rate, append a warning
716+
// to its description so the LLM avoids using it.
717+
let preferences: HashMap<_, _> = context
718+
.records
719+
.iter()
720+
.filter_map(|record| {
721+
if let SessionRecord::StrategyPreference(pref) = record {
722+
if let Some(skill_name) = &pref.skill_name {
723+
return Some((skill_name.clone(), pref.reason.clone()));
724+
}
725+
}
726+
None
727+
})
728+
.collect();
729+
730+
for def in &mut available_skills {
731+
if let Some(reason) = preferences.get(&def.manifest.name) {
732+
def.manifest.description = format!(
733+
"{} [CRITICAL WARNING: Avoid using this tool if possible. Reason: {}]",
734+
def.manifest.description, reason
735+
);
736+
}
737+
}
738+
739+
let plan = context.active_execution_plan();
698740
let provider_request = ProviderRequest {
699741
trigger: trigger.clone(),
700742
context: context.to_snapshot(steps_executed),
701743
available_skills,
702744
config: context.metadata.provider.clone(),
703745
policy: context.metadata.policy.clone(),
704-
contents: build_provider_contents(&trigger, &context.records),
746+
contents: build_provider_contents(&trigger, &context.records, plan.as_ref()),
705747
};
706748
let provider_started = Instant::now();
707749
let decision = match tokio::time::timeout(
@@ -1618,21 +1660,26 @@ impl AgentEngine {
16181660
.append_outcome(&context.session_id, outcome.clone())
16191661
.await
16201662
{
1621-
error!("failed to persist outcome: {}", err.message);
1622-
return Ok(storage_failure_outcome(
1623-
context.metadata.trigger_id.clone(),
1624-
steps_executed,
1625-
err.message,
1626-
));
1663+
warn!(session_id = %context.session_id, "failed to record outcome: {}", err.message);
16271664
}
1628-
context
1629-
.records
1630-
.push(SessionRecord::Outcome(outcome.clone()));
1631-
let session_id = context.session_id.clone();
1632-
let snapshot = context.to_snapshot(steps_executed);
1665+
1666+
context.records.push(SessionRecord::Outcome(outcome.clone()));
1667+
16331668
let outcome_clone = outcome.clone();
1634-
self.run_self_improvement(session_id, snapshot, outcome_clone)
1669+
self.run_self_improvement(context, outcome_clone)
16351670
.await?;
1671+
1672+
// Update the state projection cache AFTER self improvement
1673+
let _ = self.state_cache.set_projection(
1674+
&context.session_id,
1675+
SessionSnapshot {
1676+
session_id: context.session_id.clone(),
1677+
records: context.records.clone(),
1678+
last_sequence_no: None,
1679+
latest_outcome: Some(outcome.clone()),
1680+
}
1681+
).await;
1682+
16361683
Ok(EngineOutcome {
16371684
trigger_id: outcome.trigger_id,
16381685
stop_reason,
@@ -1645,19 +1692,20 @@ impl AgentEngine {
16451692
}
16461693

16471694
#[instrument(
1648-
skip(self, snapshot, outcome),
1695+
skip(self, context, outcome),
16491696
fields(
1650-
session_id = %session_id,
1651-
trigger_id = %snapshot.trigger_id,
1697+
session_id = %context.session_id,
1698+
trigger_id = %context.metadata.trigger_id,
16521699
stop_reason = ?outcome.stop_reason
16531700
)
16541701
)]
16551702
async fn run_self_improvement(
16561703
&self,
1657-
session_id: String,
1658-
snapshot: AgentContextSnapshot,
1704+
context: &mut AgentContext,
16591705
outcome: OutcomeRecord,
16601706
) -> Result<(), EngineError> {
1707+
let snapshot = context.to_snapshot(outcome.steps_executed);
1708+
let session_id = context.session_id.clone();
16611709
let policy = snapshot.policy.self_improvement.clone();
16621710
if !policy.enabled {
16631711
return Ok(());
@@ -1680,11 +1728,14 @@ impl AgentEngine {
16801728
self.memory
16811729
.append_reflection(&session_id, reflection.clone())
16821730
.await?;
1731+
context.records.push(SessionRecord::Reflection(reflection));
16831732

16841733
for performance in summarize_tool_performance(&snapshot.history) {
16851734
self.memory
16861735
.append_tool_performance(&session_id, performance.clone())
16871736
.await?;
1737+
context.records.push(SessionRecord::ToolPerformance(performance.clone()));
1738+
16881739
if performance.calls > 0 {
16891740
counter!(
16901741
"rain_engine.tool_performance_summaries_total",
@@ -1706,8 +1757,9 @@ impl AgentEngine {
17061757
confidence: 0.68,
17071758
};
17081759
self.memory
1709-
.append_strategy_preference(&session_id, preference)
1760+
.append_strategy_preference(&session_id, preference.clone())
17101761
.await?;
1762+
context.records.push(SessionRecord::StrategyPreference(preference));
17111763
}
17121764
}
17131765

@@ -1717,8 +1769,9 @@ impl AgentEngine {
17171769

17181770
if let Some(rollback) = maybe_rollback_regression(&snapshot, &outcome) {
17191771
self.memory
1720-
.append_policy_tuning(&session_id, rollback)
1772+
.append_policy_tuning(&session_id, rollback.clone())
17211773
.await?;
1774+
context.records.push(SessionRecord::PolicyTuning(rollback));
17221775
counter!("rain_engine.self_improvement_rollbacks_total").increment(1);
17231776
return Ok(());
17241777
}
@@ -1736,8 +1789,9 @@ impl AgentEngine {
17361789
PolicyTuningAction::Proposed | PolicyTuningAction::RolledBack => {}
17371790
}
17381791
self.memory
1739-
.append_policy_tuning(&session_id, tuning)
1792+
.append_policy_tuning(&session_id, tuning.clone())
17401793
.await?;
1794+
context.records.push(SessionRecord::PolicyTuning(tuning));
17411795

17421796
let profile_patch = ProfilePatchRecord {
17431797
patch_id: format!("profile-patch-{}", Uuid::new_v4()),
@@ -1748,8 +1802,9 @@ impl AgentEngine {
17481802
applied: true,
17491803
};
17501804
self.memory
1751-
.append_profile_patch(&session_id, profile_patch)
1805+
.append_profile_patch(&session_id, profile_patch.clone())
17521806
.await?;
1807+
context.records.push(SessionRecord::ProfilePatch(profile_patch));
17531808

17541809
Ok(())
17551810
}
@@ -1986,12 +2041,13 @@ fn maybe_rollback_regression(
19862041
if !snapshot.policy.self_improvement.rollback_on_regression {
19872042
return None;
19882043
}
2044+
// MaxStepsReached is not a regression. Hitting the step limit at a higher budget
2045+
// is progress, not a failure of the policy overlay itself.
19892046
if !matches!(
19902047
outcome.stop_reason,
19912048
StopReason::ProviderFailure
19922049
| StopReason::DeadlineExceeded
19932050
| StopReason::PolicyAborted
1994-
| StopReason::MaxStepsReached
19952051
) {
19962052
return None;
19972053
}
@@ -2048,7 +2104,11 @@ fn propose_policy_tuning(
20482104
);
20492105
}
20502106
StopReason::MaxStepsReached => {
2051-
patch.max_steps = Some(increase_usize_by_percent(snapshot.policy.max_steps, delta));
2107+
// Scale up aggressively if we keep hitting the step limit (delta, then 2x delta, etc.)
2108+
// We use the terminal observation count as a multiplier
2109+
let multiplier = terminal_observation_count(&snapshot.history).max(1) as f64;
2110+
let aggressive_delta = (delta * multiplier).min(200.0); // Cap at 200% increase per try
2111+
patch.max_steps = Some(increase_usize_by_percent(snapshot.policy.max_steps, aggressive_delta));
20522112
reason = Some(
20532113
"Session hit max steps; increasing future step budget within guardrails."
20542114
.to_string(),
@@ -2082,6 +2142,7 @@ fn propose_policy_tuning(
20822142
status: match improvement.mode {
20832143
SelfImprovementMode::Advisory => PolicyOverlayStatus::Proposed,
20842144
SelfImprovementMode::AutoWithGuardrails => PolicyOverlayStatus::Applied,
2145+
SelfImprovementMode::Shadow => PolicyOverlayStatus::Proposed, // Will be elevated to Applied by the shadow task
20852146
},
20862147
reason,
20872148
evidence_window_records: snapshot.history.len(),
@@ -2103,6 +2164,10 @@ fn propose_policy_tuning(
21032164
} else {
21042165
match improvement.mode {
21052166
SelfImprovementMode::Advisory => PolicyTuningAction::Proposed,
2167+
SelfImprovementMode::Shadow => {
2168+
overlay.status = PolicyOverlayStatus::Proposed; // Start as proposed in shadow mode
2169+
PolicyTuningAction::Proposed
2170+
},
21062171
SelfImprovementMode::AutoWithGuardrails => PolicyTuningAction::Applied,
21072172
}
21082173
};
@@ -2627,6 +2692,7 @@ fn derive_trigger_kernel_events(
26272692
fn build_provider_contents(
26282693
trigger: &AgentTrigger,
26292694
history: &[SessionRecord],
2695+
active_plan: Option<&ExecutionPlanRecord>,
26302696
) -> Vec<ProviderMessage> {
26312697
let mut messages = Vec::new();
26322698
let mut intent_by_trigger = HashMap::<String, String>::new();
@@ -2706,6 +2772,19 @@ fn build_provider_contents(
27062772
parts: build_trigger_parts(trigger),
27072773
});
27082774

2775+
// Inject Chain of Thought active plan if one exists
2776+
if let Some(plan) = active_plan {
2777+
messages.push(ProviderMessage {
2778+
role: ProviderRole::System,
2779+
parts: vec![ProviderContentPart::Text(format!(
2780+
"You are currently executing an active, multi-step plan.\n\nOBJECTIVE: {}\n\nYou are on step {} of {}.\n\nPlease stay focused on the objective and execute the necessary actions for this specific step.",
2781+
plan.objective,
2782+
plan.current_step_index + 1,
2783+
plan.steps.len()
2784+
))],
2785+
});
2786+
}
2787+
27092788
messages
27102789
}
27112790

rain-engine-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod models;
1515
mod policy;
1616
mod retrieval;
1717
mod skill_store;
18+
mod state_cache;
1819
mod traits;
1920
mod types;
2021

@@ -31,6 +32,7 @@ pub use models::*;
3132
pub use policy::*;
3233
pub use retrieval::*;
3334
pub use skill_store::*;
35+
pub use state_cache::*;
3436
#[allow(unused_imports)]
3537
pub use traits::*;
3638
#[allow(unused_imports)]

0 commit comments

Comments
 (0)