Skip to content

Commit 869a00e

Browse files
authored
Merge pull request #122 from PyRo1121/fix/panic-free-error-handling
fix: replace .expect()/.unwrap() with proper error propagation in production code
2 parents 6f36926 + f1f2cfe commit 869a00e

10 files changed

Lines changed: 153 additions & 124 deletions

File tree

src/agent/channel.rs

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ impl Channel {
391391

392392
if messages.len() == 1 {
393393
// Single message - process normally
394-
let message = messages.into_iter().next().unwrap();
394+
let message = messages.into_iter().next().ok_or_else(|| anyhow::anyhow!("empty iterator after length check"))?;
395395
self.handle_message(message).await
396396
} else {
397397
// Multiple messages - batch them
@@ -464,8 +464,7 @@ impl Channel {
464464
});
465465
self.conversation_context = Some(
466466
prompt_engine
467-
.render_conversation_context(&first.source, server_name, channel_name)
468-
.expect("failed to render conversation context"),
467+
.render_conversation_context(&first.source, server_name, channel_name)?,
469468
);
470469
}
471470

@@ -557,7 +556,7 @@ impl Channel {
557556
// Build system prompt with coalesce hint
558557
let system_prompt = self
559558
.build_system_prompt_with_coalesce(message_count, elapsed_secs, unique_sender_count)
560-
.await;
559+
.await?;
561560

562561
{
563562
let mut reply_target = self.state.reply_target_message_id.write().await;
@@ -594,21 +593,20 @@ impl Channel {
594593
message_count: usize,
595594
elapsed_secs: f64,
596595
unique_senders: usize,
597-
) -> String {
596+
) -> Result<String> {
598597
let rc = &self.deps.runtime_config;
599598
let prompt_engine = rc.prompts.load();
600599

601600
let identity_context = rc.identity.load().render();
602601
let memory_bulletin = rc.memory_bulletin.load();
603602
let skills = rc.skills.load();
604-
let skills_prompt = skills.render_channel_prompt(&prompt_engine);
603+
let skills_prompt = skills.render_channel_prompt(&prompt_engine)?;
605604

606605
let browser_enabled = rc.browser_config.load().enabled;
607606
let web_search_enabled = rc.brave_search_key.load().is_some();
608607
let opencode_enabled = rc.opencode.load().enabled;
609-
let worker_capabilities = prompt_engine
610-
.render_worker_capabilities(browser_enabled, web_search_enabled, opencode_enabled)
611-
.expect("failed to render worker capabilities");
608+
let worker_capabilities =
609+
prompt_engine.render_worker_capabilities(browser_enabled, web_search_enabled, opencode_enabled)?;
612610

613611
let status_text = {
614612
let status = self.state.status_block.read().await;
@@ -625,18 +623,16 @@ impl Channel {
625623

626624
let empty_to_none = |s: String| if s.is_empty() { None } else { Some(s) };
627625

628-
prompt_engine
629-
.render_channel_prompt(
630-
empty_to_none(identity_context),
631-
empty_to_none(memory_bulletin.to_string()),
632-
empty_to_none(skills_prompt),
633-
worker_capabilities,
634-
self.conversation_context.clone(),
635-
empty_to_none(status_text),
636-
coalesce_hint,
637-
available_channels,
638-
)
639-
.expect("failed to render channel prompt")
626+
prompt_engine.render_channel_prompt(
627+
empty_to_none(identity_context),
628+
empty_to_none(memory_bulletin.to_string()),
629+
empty_to_none(skills_prompt),
630+
worker_capabilities,
631+
self.conversation_context.clone(),
632+
empty_to_none(status_text),
633+
coalesce_hint,
634+
available_channels,
635+
)
640636
}
641637

642638
/// Handle an incoming message by running the channel's LLM agent loop.
@@ -718,12 +714,11 @@ impl Channel {
718714
});
719715
self.conversation_context = Some(
720716
prompt_engine
721-
.render_conversation_context(&message.source, server_name, channel_name)
722-
.expect("failed to render conversation context"),
717+
.render_conversation_context(&message.source, server_name, channel_name)?,
723718
);
724719
}
725720

726-
let system_prompt = self.build_system_prompt().await;
721+
let system_prompt = self.build_system_prompt().await?;
727722

728723
{
729724
let mut reply_target = self.state.reply_target_message_id.write().await;
@@ -795,21 +790,20 @@ impl Channel {
795790
}
796791

797792
/// Assemble the full system prompt using the PromptEngine.
798-
async fn build_system_prompt(&self) -> String {
793+
async fn build_system_prompt(&self) -> crate::error::Result<String> {
799794
let rc = &self.deps.runtime_config;
800795
let prompt_engine = rc.prompts.load();
801796

802797
let identity_context = rc.identity.load().render();
803798
let memory_bulletin = rc.memory_bulletin.load();
804799
let skills = rc.skills.load();
805-
let skills_prompt = skills.render_channel_prompt(&prompt_engine);
800+
let skills_prompt = skills.render_channel_prompt(&prompt_engine)?;
806801

807802
let browser_enabled = rc.browser_config.load().enabled;
808803
let web_search_enabled = rc.brave_search_key.load().is_some();
809804
let opencode_enabled = rc.opencode.load().enabled;
810805
let worker_capabilities = prompt_engine
811-
.render_worker_capabilities(browser_enabled, web_search_enabled, opencode_enabled)
812-
.expect("failed to render worker capabilities");
806+
.render_worker_capabilities(browser_enabled, web_search_enabled, opencode_enabled)?;
813807

814808
let status_text = {
815809
let status = self.state.status_block.read().await;
@@ -831,7 +825,6 @@ impl Channel {
831825
None, // coalesce_hint - only set for batched messages
832826
available_channels,
833827
)
834-
.expect("failed to render channel prompt")
835828
}
836829

837830
/// Register per-turn tools, run the LLM agentic loop, and clean up.
@@ -1177,13 +1170,19 @@ impl Channel {
11771170
"firing debounced retrigger"
11781171
);
11791172

1180-
let retrigger_message = self
1173+
let retrigger_message = match self
11811174
.deps
11821175
.runtime_config
11831176
.prompts
11841177
.load()
11851178
.render_system_retrigger()
1186-
.expect("failed to render retrigger message");
1179+
{
1180+
Ok(message) => message,
1181+
Err(error) => {
1182+
tracing::error!(%error, "failed to render retrigger message");
1183+
return;
1184+
}
1185+
};
11871186

11881187
let synthetic = InboundMessage {
11891188
id: uuid::Uuid::new_v4().to_string(),
@@ -1255,7 +1254,7 @@ pub async fn spawn_branch_from_state(
12551254
&rc.instance_dir.display().to_string(),
12561255
&rc.workspace_dir.display().to_string(),
12571256
)
1258-
.expect("failed to render branch prompt");
1257+
.map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))?;
12591258

12601259
spawn_branch(
12611260
state,
@@ -1279,10 +1278,10 @@ async fn spawn_memory_persistence_branch(
12791278
let prompt_engine = deps.runtime_config.prompts.load();
12801279
let system_prompt = prompt_engine
12811280
.render_static("memory_persistence")
1282-
.expect("failed to render memory_persistence prompt");
1281+
.map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))?;
12831282
let prompt = prompt_engine
12841283
.render_system_memory_persistence()
1285-
.expect("failed to render memory persistence prompt");
1284+
.map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))?;
12861285

12871286
spawn_branch(
12881287
state,
@@ -1413,7 +1412,7 @@ pub async fn spawn_worker_from_state(
14131412
&rc.instance_dir.display().to_string(),
14141413
&rc.workspace_dir.display().to_string(),
14151414
)
1416-
.expect("failed to render worker prompt");
1415+
.map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))?;
14171416
let skills = rc.skills.load();
14181417
let browser_config = (**rc.browser_config.load()).clone();
14191418
let brave_search_key = (**rc.brave_search_key.load()).clone();

src/agent/compactor.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,15 @@ impl Compactor {
108108
let channel_id = self.channel_id.clone();
109109
let deps = self.deps.clone();
110110
let prompt_engine = deps.runtime_config.prompts.load();
111-
let compactor_prompt = prompt_engine
112-
.render_static("compactor")
113-
.expect("failed to render compactor prompt");
111+
let compactor_prompt = match prompt_engine.render_static("compactor") {
112+
Ok(p) => p,
113+
Err(error) => {
114+
tracing::error!(%error, "failed to render compactor prompt");
115+
let mut flag = is_compacting.write().await;
116+
*flag = false;
117+
return;
118+
}
119+
};
114120

115121
tokio::spawn(async move {
116122
let result = run_compaction(&deps, &compactor_prompt, &history, fraction).await;
@@ -155,9 +161,7 @@ impl Compactor {
155161

156162
// Insert a marker at the beginning
157163
let prompt_engine = self.deps.runtime_config.prompts.load();
158-
let marker = prompt_engine
159-
.render_system_truncation(remove_count)
160-
.expect("failed to render truncation message");
164+
let marker = prompt_engine.render_system_truncation(remove_count)?;
161165
history.insert(0, Message::from(marker));
162166

163167
tracing::warn!(

src/agent/cortex.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,9 +456,13 @@ pub async fn generate_bulletin(deps: &AgentDeps, logger: &CortexLogger) -> bool
456456
// Phase 2: LLM synthesis of raw sections into a cohesive bulletin
457457
let cortex_config = **deps.runtime_config.cortex.load();
458458
let prompt_engine = deps.runtime_config.prompts.load();
459-
let bulletin_prompt = prompt_engine
460-
.render_static("cortex_bulletin")
461-
.expect("failed to render cortex bulletin prompt");
459+
let bulletin_prompt = match prompt_engine.render_static("cortex_bulletin") {
460+
Ok(p) => p,
461+
Err(error) => {
462+
tracing::error!(%error, "failed to render cortex bulletin prompt");
463+
return false;
464+
}
465+
};
462466

463467
let routing = deps.runtime_config.routing.load();
464468
let model_name = routing.resolve(ProcessType::Branch, None).to_string();
@@ -468,9 +472,15 @@ pub async fn generate_bulletin(deps: &AgentDeps, logger: &CortexLogger) -> bool
468472
// No tools needed — the LLM just synthesizes the pre-gathered data
469473
let agent = AgentBuilder::new(model).preamble(&bulletin_prompt).build();
470474

471-
let synthesis_prompt = prompt_engine
475+
let synthesis_prompt = match prompt_engine
472476
.render_system_cortex_synthesis(cortex_config.bulletin_max_words, &raw_sections)
473-
.expect("failed to render cortex synthesis prompt");
477+
{
478+
Ok(p) => p,
479+
Err(error) => {
480+
tracing::error!(%error, "failed to render cortex synthesis prompt");
481+
return false;
482+
}
483+
};
474484

475485
match agent.prompt(&synthesis_prompt).await {
476486
Ok(bulletin) => {

src/agent/cortex_chat.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ impl CortexChatSession {
241241
.await?;
242242

243243
// Build the system prompt
244-
let system_prompt = self.build_system_prompt(channel_context_id).await;
244+
let system_prompt = self.build_system_prompt(channel_context_id).await?;
245245

246246
// Load chat history and convert to Rig messages
247247
let chat_messages = self.store.load_history(thread_id, 100).await?;
@@ -313,7 +313,7 @@ impl CortexChatSession {
313313
Ok(event_rx)
314314
}
315315

316-
async fn build_system_prompt(&self, channel_context_id: Option<&str>) -> String {
316+
async fn build_system_prompt(&self, channel_context_id: Option<&str>) -> crate::error::Result<String> {
317317
let runtime_config = &self.deps.runtime_config;
318318
let prompt_engine = runtime_config.prompts.load();
319319

@@ -323,9 +323,8 @@ impl CortexChatSession {
323323
let browser_enabled = runtime_config.browser_config.load().enabled;
324324
let web_search_enabled = runtime_config.brave_search_key.load().is_some();
325325
let opencode_enabled = runtime_config.opencode.load().enabled;
326-
let worker_capabilities = prompt_engine
327-
.render_worker_capabilities(browser_enabled, web_search_enabled, opencode_enabled)
328-
.expect("failed to render worker capabilities");
326+
let worker_capabilities =
327+
prompt_engine.render_worker_capabilities(browser_enabled, web_search_enabled, opencode_enabled)?;
329328

330329
// Load channel transcript if a channel context is active
331330
let channel_transcript = if let Some(channel_id) = channel_context_id {
@@ -336,14 +335,12 @@ impl CortexChatSession {
336335

337336
let empty_to_none = |s: String| if s.is_empty() { None } else { Some(s) };
338337

339-
prompt_engine
340-
.render_cortex_chat_prompt(
341-
empty_to_none(identity_context),
342-
empty_to_none(memory_bulletin.to_string()),
343-
channel_transcript,
344-
worker_capabilities,
345-
)
346-
.expect("failed to render cortex chat prompt")
338+
prompt_engine.render_cortex_chat_prompt(
339+
empty_to_none(identity_context),
340+
empty_to_none(memory_bulletin.to_string()),
341+
channel_transcript,
342+
worker_capabilities,
343+
)
347344
}
348345

349346
/// Load the last 50 messages from a channel as a formatted transcript.

src/agent/ingestion.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -460,9 +460,7 @@ async fn process_chunk(
460460
deps: &AgentDeps,
461461
) -> anyhow::Result<()> {
462462
let prompt_engine = deps.runtime_config.prompts.load();
463-
let ingestion_prompt = prompt_engine
464-
.render_static("ingestion")
465-
.expect("failed to render ingestion prompt");
463+
let ingestion_prompt = prompt_engine.render_static("ingestion")?;
466464

467465
let routing = deps.runtime_config.routing.load();
468466
let model_name = routing.resolve(ProcessType::Branch, None).to_string();
@@ -484,9 +482,8 @@ async fn process_chunk(
484482
.tool_server_handle(tool_server)
485483
.build();
486484

487-
let user_prompt = prompt_engine
488-
.render_system_ingestion_chunk(filename, chunk_number, total_chunks, chunk)
489-
.expect("failed to render ingestion chunk prompt");
485+
let user_prompt =
486+
prompt_engine.render_system_ingestion_chunk(filename, chunk_number, total_chunks, chunk)?;
490487

491488
let mut history = Vec::new();
492489
match agent.prompt(&user_prompt).with_history(&mut history).await {

src/agent/worker.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,8 @@ impl Worker {
357357
self.hook.send_status("compacting (overflow recovery)");
358358
self.force_compact_history(&mut history).await;
359359
let prompt_engine = self.deps.runtime_config.prompts.load();
360-
let overflow_msg = prompt_engine
361-
.render_system_worker_overflow()
362-
.expect("failed to render worker overflow message");
360+
let overflow_msg =
361+
prompt_engine.render_system_worker_overflow()?;
363362
follow_up_prompt = format!("{follow_up}\n\n{overflow_msg}");
364363
}
365364
Err(error) => {
@@ -449,9 +448,13 @@ impl Worker {
449448

450449
let recap = build_worker_recap(&removed);
451450
let prompt_engine = self.deps.runtime_config.prompts.load();
452-
let marker = prompt_engine
453-
.render_system_worker_compact(remove_count, &recap)
454-
.expect("failed to render worker compact message");
451+
let marker = match prompt_engine.render_system_worker_compact(remove_count, &recap) {
452+
Ok(m) => m,
453+
Err(error) => {
454+
tracing::error!(%error, "failed to render worker compact marker");
455+
return;
456+
}
457+
};
455458
history.insert(0, rig::message::Message::from(marker));
456459

457460
tracing::info!(

0 commit comments

Comments
 (0)