Plan–Act–Observe Loop
The Plan→Act→Observe loop is the fundamental reasoning pattern for agents in Scarab-Runtime.
State Sequence
Init
↓
Plan ← Reason, declare steps, read memory
↓
Act ← Invoke tools, make changes
↓
Observe ← Record what happened, update memory
↓
Plan (repeat, or)
↓
Terminate
Plan Phase
In the Plan phase, the agent reasons about what to do next:
#![allow(unused)] fn main() { agent.transition(LifecycleState::Plan).await?; // Read prior context from memory let prior_results = agent.memory_get("prior_results").await?; // Declare the plan (advisory - helps deviation detection) agent.plan(vec![ PlanStep { description: "Search for relevant papers".to_string(), tool: Some("web.search".to_string()), }, PlanStep { description: "Summarize top 3 results".to_string(), tool: Some("lm.complete".to_string()), }, PlanStep { description: "Write summary to workspace".to_string(), tool: Some("fs.write".to_string()), }, ]).await?; }
The plan is advisory; the daemon uses it for deviation detection in Strict mode but does not enforce sequential execution.
Act Phase
In the Act phase, the agent executes its plan:
#![allow(unused)] fn main() { agent.transition(LifecycleState::Act).await?; // Step 1: Search let search_result = agent.invoke_tool("web.search", json!({ "query": "renewable energy breakthroughs 2026" })).await?; // Step 2: Summarize let summary = agent.invoke_tool("lm.complete", json!({ "prompt": format!("Summarize these search results: {}", search_result["results"]), "model": model })).await?; // Step 3: Write agent.invoke_tool("fs.write", json!({ "path": "/workspace/summary.md", "content": summary["text"] })).await?; }
Observe Phase
In the Observe phase, the agent records what happened and updates memory:
#![allow(unused)] fn main() { agent.transition(LifecycleState::Observe).await?; // Record observation agent.observe( "completed_summary", &format!("Summary written: {} tokens used", summary["output_tokens"]), vec!["success".to_string(), "summary".to_string()], ).await?; // Update persistent memory agent.memory_set("last_summary_path", json!("/workspace/summary.md")).await?; agent.memory_set("run_count", json!(run_count + 1)).await?; }
Full Example Loop
#![allow(unused)] fn main() { let task = std::env::var("SCARAB_TASK").unwrap_or_default(); let model = std::env::var("SCARAB_MODEL").unwrap_or_else(|_| "anthropic/claude-opus-4-6".to_string()); agent.transition(LifecycleState::Plan).await?; agent.plan(vec![/* ... */]).await?; agent.transition(LifecycleState::Act).await?; let result = agent.invoke_tool("lm.complete", json!({ "prompt": task, "model": model })).await?; agent.transition(LifecycleState::Observe).await?; agent.observe("completed", &format!("Done: {}", result["text"]), vec![]).await?; agent.transition(LifecycleState::Terminate).await?; }
Plan Revision
If the agent discovers mid-execution that the original plan needs changing:
#![allow(unused)] fn main() { // Still in Act phase, but plan needs updating agent.revise_plan( new_steps, "Found additional data source that requires an extra step".to_string(), ).await?; }
Revisions create an audit entry with the reason, enabling full traceability of reasoning changes.
Multi-Iteration Loop
For long-running agents that iterate:
#![allow(unused)] fn main() { loop { // Check for escalations before each iteration let escalations = agent.pending_escalations().await?; if !escalations.is_empty() { handle_escalations(&mut agent, escalations).await?; } agent.transition(LifecycleState::Plan).await?; // ... plan ... agent.transition(LifecycleState::Act).await?; // ... act ... agent.transition(LifecycleState::Observe).await?; // ... observe ... if task_complete { break; } } agent.transition(LifecycleState::Terminate).await?; }