Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/openfang-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,11 @@ pub async fn send_message(
// (not as a separate session message which the LLM may not process).
let content_blocks = if !req.attachments.is_empty() {
let image_blocks = resolve_attachments(&req.attachments);
if image_blocks.is_empty() { None } else { Some(image_blocks) }
if image_blocks.is_empty() {
None
} else {
Some(image_blocks)
}
} else {
None
};
Expand Down
9 changes: 6 additions & 3 deletions crates/openfang-channels/src/whatsapp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ impl ChannelAdapter for WhatsAppAdapter {
"https://graph.facebook.com/v21.0/{}/messages",
self.phone_number_id
);
let resp = self.client
let resp = self
.client
.post(&api_url)
.bearer_auth(&*self.access_token)
.json(&body)
Expand All @@ -284,7 +285,8 @@ impl ChannelAdapter for WhatsAppAdapter {
"https://graph.facebook.com/v21.0/{}/messages",
self.phone_number_id
);
let resp = self.client
let resp = self
.client
.post(&api_url)
.bearer_auth(&*self.access_token)
.json(&body)
Expand All @@ -310,7 +312,8 @@ impl ChannelAdapter for WhatsAppAdapter {
"https://graph.facebook.com/v21.0/{}/messages",
self.phone_number_id
);
let resp = self.client
let resp = self
.client
.post(&api_url)
.bearer_auth(&*self.access_token)
.json(&body)
Expand Down
23 changes: 14 additions & 9 deletions crates/openfang-kernel/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2003,19 +2003,24 @@ impl OpenFangKernel {
// Persist usage to database (same as non-streaming path)
let model = &manifest.model.model;
let cost = MeteringEngine::estimate_cost_with_catalog(
&kernel_clone.model_catalog.read().unwrap_or_else(|e| e.into_inner()),
&kernel_clone
.model_catalog
.read()
.unwrap_or_else(|e| e.into_inner()),
model,
result.total_usage.input_tokens,
result.total_usage.output_tokens,
);
let _ = kernel_clone.metering.record(&openfang_memory::usage::UsageRecord {
agent_id,
model: model.clone(),
input_tokens: result.total_usage.input_tokens,
output_tokens: result.total_usage.output_tokens,
cost_usd: cost,
tool_calls: result.iterations.saturating_sub(1),
});
let _ = kernel_clone
.metering
.record(&openfang_memory::usage::UsageRecord {
agent_id,
model: model.clone(),
input_tokens: result.total_usage.input_tokens,
output_tokens: result.total_usage.output_tokens,
cost_usd: cost,
tool_calls: result.iterations.saturating_sub(1),
});

let _ = kernel_clone
.registry
Expand Down
12 changes: 2 additions & 10 deletions crates/openfang-memory/src/knowledge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,7 @@ impl KnowledgeStore {
let mut idx = 1;

if let Some(ref source) = pattern.source {
sql.push_str(&format!(
" AND (s.id = ?{} OR s.name = ?{})",
idx,
idx + 1
));
sql.push_str(&format!(" AND (s.id = ?{} OR s.name = ?{})", idx, idx + 1));
params.push(Box::new(source.clone()));
params.push(Box::new(source.clone()));
idx += 2;
Expand All @@ -117,11 +113,7 @@ impl KnowledgeStore {
idx += 1;
}
if let Some(ref target) = pattern.target {
sql.push_str(&format!(
" AND (t.id = ?{} OR t.name = ?{})",
idx,
idx + 1
));
sql.push_str(&format!(" AND (t.id = ?{} OR t.name = ?{})", idx, idx + 1));
params.push(Box::new(target.clone()));
params.push(Box::new(target.clone()));
idx += 2;
Expand Down
34 changes: 27 additions & 7 deletions crates/openfang-runtime/src/agent_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,15 @@ fn phantom_action_detected(text: &str) -> bool {
let lower = text.to_lowercase();
let action_verbs = ["sent ", "posted ", "emailed ", "delivered ", "forwarded "];
let channel_refs = [
"telegram", "whatsapp", "slack", "discord", "email", "channel",
"message sent", "successfully sent", "has been sent",
"telegram",
"whatsapp",
"slack",
"discord",
"email",
"channel",
"message sent",
"successfully sent",
"has been sent",
];
let has_action = action_verbs.iter().any(|v| lower.contains(v));
let has_channel = channel_refs.iter().any(|c| lower.contains(c));
Expand Down Expand Up @@ -272,7 +279,9 @@ pub async fn run_agent_loop(
// The LLM already received them via llm_messages above.
for msg in session.messages.iter_mut() {
if let MessageContent::Blocks(blocks) = &mut msg.content {
let had_images = blocks.iter().any(|b| matches!(b, ContentBlock::Image { .. }));
let had_images = blocks
.iter()
.any(|b| matches!(b, ContentBlock::Image { .. }));
if had_images {
blocks.retain(|b| !matches!(b, ContentBlock::Image { .. }));
if blocks.is_empty() {
Expand Down Expand Up @@ -454,7 +463,10 @@ pub async fn run_agent_loop(
// One-shot retry: if the LLM returns empty text with no tool use,
// try once more before accepting the empty result.
// Triggers on first call OR when input_tokens=0 (silently failed request).
if text.trim().is_empty() && response.tool_calls.is_empty() && !response.has_any_content() {
if text.trim().is_empty()
&& response.tool_calls.is_empty()
&& !response.has_any_content()
{
let is_silent_failure =
response.usage.input_tokens == 0 && response.usage.output_tokens == 0;
if iteration == 0 || is_silent_failure {
Expand Down Expand Up @@ -499,7 +511,10 @@ pub async fn run_agent_loop(
// channel action (send, post, email, etc.) but never actually
// called the corresponding tool, re-prompt once to force real
// tool usage instead of hallucinated completion.
let text = if !any_tools_executed && iteration == 0 && phantom_action_detected(&text) {
let text = if !any_tools_executed
&& iteration == 0
&& phantom_action_detected(&text)
{
warn!(agent = %manifest.name, "Phantom action detected — re-prompting for real tool use");
messages.push(Message::assistant(text));
messages.push(Message::user(
Expand Down Expand Up @@ -1275,7 +1290,9 @@ pub async fn run_agent_loop_streaming(
// The LLM already received them via llm_messages above.
for msg in session.messages.iter_mut() {
if let MessageContent::Blocks(blocks) = &mut msg.content {
let had_images = blocks.iter().any(|b| matches!(b, ContentBlock::Image { .. }));
let had_images = blocks
.iter()
.any(|b| matches!(b, ContentBlock::Image { .. }));
if had_images {
blocks.retain(|b| !matches!(b, ContentBlock::Image { .. }));
if blocks.is_empty() {
Expand Down Expand Up @@ -1475,7 +1492,10 @@ pub async fn run_agent_loop_streaming(
// One-shot retry: if the LLM returns empty text with no tool use,
// try once more before accepting the empty result.
// Triggers on first call OR when input_tokens=0 (silently failed request).
if text.trim().is_empty() && response.tool_calls.is_empty() && !response.has_any_content() {
if text.trim().is_empty()
&& response.tool_calls.is_empty()
&& !response.has_any_content()
{
let is_silent_failure =
response.usage.input_tokens == 0 && response.usage.output_tokens == 0;
if iteration == 0 || is_silent_failure {
Expand Down
5 changes: 2 additions & 3 deletions crates/openfang-runtime/src/drivers/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,9 +471,8 @@ impl LlmDriver for AnthropicDriver {
input_json,
}) = blocks.get(block_idx)
{
let input: serde_json::Value =
serde_json::from_str(input_json)
.unwrap_or_else(|_| serde_json::json!({}));
let input: serde_json::Value = serde_json::from_str(input_json)
.unwrap_or_else(|_| serde_json::json!({}));
let _ = tx
.send(StreamEvent::ToolUseEnd {
id: id.clone(),
Expand Down
Loading
Loading