mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-29 17:09:31 +00:00
feat(engine): platform self-awareness, event pipeline fix, globals() builtin, prompt templates
Session 9 changes driven by live trace analysis: - CodeAct event pipeline: handle_execute_code_step now transfers CodeExecutionResult events to thread.events and broadcasts via event_tx (fixes false-positive no_tools_used trace warnings) - Monty globals()/locals() builtins: returns dict of available action names from capability leases, enabling "tool_name" in globals() probing - PlatformInfo injection into system prompts (version, LLM backend, model, database, channels, owner, repo URL) - Mission goal prompts moved to prompts/*.md files (include_str! pattern) - /expected command for triggering self-improvement from user feedback - Session 9 development history Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
@@ -144,6 +144,7 @@ All commands parsed by `SubmissionParser::parse()`:
|
||||
| `/heartbeat` | `Heartbeat` | |
|
||||
| `/summarize`, `/summary` | `Summarize` | |
|
||||
| `/suggest` | `Suggest` | |
|
||||
| `/expected <desc>` | `Expected` | Fires self-improvement with conversation context |
|
||||
| `/new`, `/thread new` | `NewThread` | |
|
||||
| `/thread <uuid>` | `SwitchThread` | Must be valid UUID |
|
||||
| `/resume <uuid>` | `Resume` | Must be valid UUID |
|
||||
|
||||
+37
-1
@@ -203,6 +203,9 @@ pub struct Agent {
|
||||
/// the engine to gateway/manual trigger entry points.
|
||||
pub(super) routine_engine_slot:
|
||||
Arc<tokio::sync::RwLock<Option<Arc<crate::agent::routine_engine::RoutineEngine>>>>,
|
||||
/// Engine v2 mission manager for firing learning missions (set after engine init).
|
||||
pub(crate) mission_manager_slot:
|
||||
Arc<tokio::sync::RwLock<Option<Arc<ironclaw_engine::MissionManager>>>>,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
@@ -274,6 +277,7 @@ impl Agent {
|
||||
hygiene_config,
|
||||
routine_config,
|
||||
routine_engine_slot: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
mission_manager_slot: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,10 +290,21 @@ impl Agent {
|
||||
self.routine_engine_slot = slot;
|
||||
}
|
||||
|
||||
async fn routine_engine(&self) -> Option<Arc<crate::agent::routine_engine::RoutineEngine>> {
|
||||
pub(super) async fn routine_engine(&self) -> Option<Arc<crate::agent::routine_engine::RoutineEngine>> {
|
||||
self.routine_engine_slot.read().await.clone()
|
||||
}
|
||||
|
||||
/// Set the engine v2 mission manager (called after engine init).
|
||||
pub async fn set_mission_manager(&self, mgr: Arc<ironclaw_engine::MissionManager>) {
|
||||
*self.mission_manager_slot.write().await = Some(mgr);
|
||||
}
|
||||
|
||||
pub(crate) async fn mission_manager(
|
||||
&self,
|
||||
) -> Option<Arc<ironclaw_engine::MissionManager>> {
|
||||
self.mission_manager_slot.read().await.clone()
|
||||
}
|
||||
|
||||
// Convenience accessors
|
||||
|
||||
/// Get the scheduler (for external wiring, e.g. CreateJobTool).
|
||||
@@ -326,6 +341,23 @@ impl Agent {
|
||||
&self.deps.hooks
|
||||
}
|
||||
|
||||
/// Build platform metadata for self-awareness in system prompts.
|
||||
pub(crate) async fn platform_info(&self) -> ironclaw_engine::PlatformInfo {
|
||||
let active_channels = self.channels.channel_names().await;
|
||||
let database_backend = std::env::var("DATABASE_BACKEND")
|
||||
.ok()
|
||||
.or_else(|| self.deps.store.as_ref().map(|_| "postgres".to_string()));
|
||||
ironclaw_engine::PlatformInfo {
|
||||
version: Some(env!("CARGO_PKG_VERSION").to_string()),
|
||||
llm_backend: Some(self.deps.llm_backend.clone()),
|
||||
model_name: Some(self.deps.llm.active_model_name()),
|
||||
database_backend,
|
||||
active_channels,
|
||||
owner_id: Some(self.deps.owner_id.clone()),
|
||||
repo_url: Some("https://github.com/nearai/ironclaw".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn cost_guard(&self) -> &Arc<crate::agent::cost_guard::CostGuard> {
|
||||
&self.deps.cost_guard
|
||||
}
|
||||
@@ -1456,6 +1488,10 @@ impl Agent {
|
||||
Submission::Heartbeat => self.process_heartbeat().await,
|
||||
Submission::Summarize => self.process_summarize(session, thread_id).await,
|
||||
Submission::Suggest => self.process_suggest(session, thread_id).await,
|
||||
Submission::Expected { description } => {
|
||||
self.process_expected(session, thread_id, &description, &message.user_id)
|
||||
.await
|
||||
}
|
||||
Submission::JobStatus { job_id } => {
|
||||
self.process_job_status(&tenant, job_id.as_deref()).await
|
||||
}
|
||||
|
||||
@@ -472,6 +472,109 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle `/expected <description>` — capture expected behavior and fire into
|
||||
/// the self-improvement pipeline.
|
||||
///
|
||||
/// Collects recent conversation turns (user input, tool calls, responses) and
|
||||
/// packages them with the user's description of what should have happened.
|
||||
/// This fires a `user_feedback:expected_behavior` system event that the
|
||||
/// expected-behavior learning mission picks up.
|
||||
pub(super) async fn process_expected(
|
||||
&self,
|
||||
session: Arc<Mutex<Session>>,
|
||||
thread_id: Uuid,
|
||||
description: &str,
|
||||
user_id: &str,
|
||||
) -> Result<SubmissionResult, Error> {
|
||||
// Extract recent turns from the session (last 5 turns for context)
|
||||
let recent_context = {
|
||||
let sess = session.lock().await;
|
||||
let thread = sess
|
||||
.threads
|
||||
.get(&thread_id)
|
||||
.ok_or_else(|| Error::from(crate::error::JobError::NotFound { id: thread_id }))?;
|
||||
|
||||
let turns: Vec<serde_json::Value> = thread
|
||||
.turns
|
||||
.iter()
|
||||
.rev()
|
||||
.take(5)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|turn| {
|
||||
let tool_calls: Vec<serde_json::Value> = turn
|
||||
.tool_calls
|
||||
.iter()
|
||||
.map(|tc| {
|
||||
serde_json::json!({
|
||||
"tool": tc.name,
|
||||
"error": tc.error,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::json!({
|
||||
"user_input": turn.user_input,
|
||||
"response": turn.response,
|
||||
"tool_calls": tool_calls,
|
||||
"state": format!("{:?}", turn.state),
|
||||
"error": turn.error,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
turns
|
||||
};
|
||||
|
||||
if recent_context.is_empty() {
|
||||
return Ok(SubmissionResult::ok_with_message(
|
||||
"No conversation history to attach feedback to.",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"expected_behavior": description,
|
||||
"thread_id": thread_id.to_string(),
|
||||
"recent_turns": recent_context,
|
||||
});
|
||||
|
||||
// Fire into v2 mission manager (learning missions)
|
||||
let mut fired: usize = 0;
|
||||
if let Some(mgr) = self.mission_manager().await {
|
||||
match mgr
|
||||
.fire_on_system_event(
|
||||
"user_feedback",
|
||||
"expected_behavior",
|
||||
user_id,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(ids) => fired += ids.len(),
|
||||
Err(e) => {
|
||||
tracing::debug!("failed to fire expected-behavior mission: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also fire through v1 routine engine (if routines listen for this)
|
||||
if let Some(engine) = self.routine_engine().await {
|
||||
fired += engine
|
||||
.emit_system_event("user_feedback", "expected_behavior", &payload, Some(user_id))
|
||||
.await;
|
||||
}
|
||||
|
||||
if fired > 0 {
|
||||
Ok(SubmissionResult::ok_with_message(format!(
|
||||
"Feedback captured. Fired {fired} self-improvement thread(s) to investigate."
|
||||
)))
|
||||
} else {
|
||||
Ok(SubmissionResult::ok_with_message(
|
||||
"Feedback noted but no self-improvement missions are configured to handle it. \
|
||||
The engine will use this context in future learning cycles.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle `/reasoning [N|all]` — show reasoning history for the active thread.
|
||||
pub(super) async fn handle_reasoning_command(
|
||||
&self,
|
||||
|
||||
@@ -127,7 +127,8 @@ impl Agent {
|
||||
let mut reasoning = Reasoning::new(self.llm().clone())
|
||||
.with_channel(message.channel.clone())
|
||||
.with_model_name(self.llm().active_model_name())
|
||||
.with_group_chat(is_group_chat);
|
||||
.with_group_chat(is_group_chat)
|
||||
.with_platform_info(self.platform_info().await);
|
||||
|
||||
// Pass channel-specific conversation context to the LLM.
|
||||
// This helps the agent know who/group it's talking to.
|
||||
|
||||
@@ -41,6 +41,12 @@ impl SubmissionParser {
|
||||
if lower == "/suggest" {
|
||||
return Submission::Suggest;
|
||||
}
|
||||
if lower.starts_with("/expected ") {
|
||||
let description = trimmed["/expected ".len()..].trim().to_string();
|
||||
if !description.is_empty() {
|
||||
return Submission::Expected { description };
|
||||
}
|
||||
}
|
||||
if lower == "/thread new" || lower == "/new" {
|
||||
return Submission::NewThread;
|
||||
}
|
||||
@@ -271,6 +277,13 @@ pub enum Submission {
|
||||
/// Suggest next steps based on the current thread.
|
||||
Suggest,
|
||||
|
||||
/// User-provided expected behavior for the last interaction.
|
||||
/// Fires into the self-improvement pipeline with conversation context.
|
||||
Expected {
|
||||
/// What the user expected to happen.
|
||||
description: String,
|
||||
},
|
||||
|
||||
/// Check job status. No job_id shows all jobs; with job_id shows a specific job.
|
||||
JobStatus {
|
||||
/// Optional job ID (UUID or short prefix). If None, shows all jobs.
|
||||
@@ -867,4 +880,20 @@ mod tests {
|
||||
assert!(matches!(SubmissionParser::parse("/QUIT"), Submission::Quit));
|
||||
assert!(matches!(SubmissionParser::parse("/Exit"), Submission::Quit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_expected() {
|
||||
let submission =
|
||||
SubmissionParser::parse("/expected should have logged in via GitHub OAuth");
|
||||
assert!(
|
||||
matches!(submission, Submission::Expected { description } if description == "should have logged in via GitHub OAuth")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_expected_empty_is_user_input() {
|
||||
// "/expected " with no description should fall through to user input
|
||||
let submission = SubmissionParser::parse("/expected ");
|
||||
assert!(matches!(submission, Submission::UserInput { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user