diff --git a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs index b093786503..fffa7462ac 100644 --- a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs @@ -35,6 +35,8 @@ use crate::agentic::tools::product_runtime::{ use crate::agentic::tools::{ resolve_tool_manifest, tool_context_runtime, ResolvedToolManifest, ToolRuntimeRestrictions, }; +use crate::agentic::tools::post_call_hooks::run_stop_hooks_for_round; +use bitfun_agent_runtime::post_call_hooks::ToolCallSummary; use crate::agentic::WorkspaceBinding; use crate::infrastructure::ai::get_global_ai_client_factory; use crate::service::config::get_global_config_service; @@ -53,8 +55,8 @@ use bitfun_core_types::SessionModelBindingPolicy; use log::{debug, error, info, trace, warn}; use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; -use std::path::Path; use std::sync::Arc; +use std::path::Path; use tokio_util::sync::CancellationToken; use tool_runtime::context::PrimaryModelFacts; @@ -3321,6 +3323,184 @@ impl ExecutionEngine { total_tools += round_result.tool_calls.len(); + // ── Stop hook: B01 提示蜂 + C01 审查蜂 ── + { + let tool_calls: Vec = round_result + .tool_calls + .iter() + .map(|tc| { + let preview = serde_json::to_string(&tc.arguments) + .unwrap_or_default(); + let preview = if preview.len() > 256 { + format!("{}...", &preview[..253]) + } else { + preview + }; + ToolCallSummary { + tool_name: tc.tool_name.clone(), + is_error: tc.is_error, + input_preview: preview, + } + }) + .collect(); + + let assistant_text = match &round_result.assistant_message.content { + MessageContent::Text(t) => t.as_str(), + MessageContent::Multimodal { text, .. } => text.as_str(), + MessageContent::Mixed { text, .. } => text.as_str(), + MessageContent::ToolResult { .. } => "", + }; + + // Collect file reads/edits from the FILE_READ_TRACKER + // For now, extract from round_result tool calls + let file_reads: Vec = round_result + .tool_calls + .iter() + .filter(|tc| matches!(tc.tool_name.as_str(), "Read" | "read_file")) + .filter_map(|tc| { + tc.arguments + .get("file_path") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }) + .collect(); + + let file_edits: Vec = round_result + .tool_calls + .iter() + .filter(|tc| { + matches!( + tc.tool_name.as_str(), + "Edit" | "Write" | "edit_file" | "write_file" + ) + }) + .filter_map(|tc| { + tc.arguments + .get("file_path") + .or_else(|| tc.arguments.get("path")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }) + .collect(); + + let aggregated = run_stop_hooks_for_round( + &self.session_manager, + &context.session_id, + &context.dialog_turn_id, + round_index as u32, + tool_calls.clone(), + assistant_text, + file_reads.clone(), + file_edits.clone(), + round_result.has_more_rounds, + ); + + // 审查蜂提醒模式:Abort 已在 post_call_hooks 降级为 WARN, + // 此处仅注入提醒上下文,不拦截任何操作 + if aggregated.is_abort() { + if let Some(abort) = &aggregated.abort { + match abort { + bitfun_agent_runtime::post_call_hooks::HookResult::Abort { + reason, + fix_instruction, + .. + } => { + let remind = format!( + "[审查蜂提醒] {reason}\n\n建议: {fix_instruction}" + ); + let msg = + Message::system(render_system_reminder(&remind)); + messages.push(msg); + warn!( + "[Stop Hook] Round {}: Reminder — {}", + round_index, reason + ); + } + _ => {} + } + } + } + + // Inject B01 additional contexts — replaces previous review messages + // to prevent unbounded context growth and spurious compression triggers. + // Bee-review messages are identified by the review-prefix inside a + // block. + messages.retain(|m| { + match &m.content { + crate::agentic::core::message::MessageContent::Text(text) => { + let trimmed = text.trim(); + !(trimmed.contains("[书记官]") && trimmed.contains("")) + && !(trimmed.contains("[提示蜂]") && trimmed.contains("")) + && !(trimmed.contains("[审查员]") && trimmed.contains("")) + } + _ => true, + } + }); + for ctx_msg in &aggregated.additional_contexts { + let msg = Message::system(render_system_reminder(ctx_msg)); + messages.push(msg); + } + + // ── 蜂群审查(cc-haha模式:stop hook → fast LLM → 注入结果)── + if !tool_calls.is_empty() { + let tl: Vec = tool_calls.iter() + .map(|tc| format!("{}{}", tc.tool_name, if tc.is_error { "(FAIL)" } else { "" })) + .collect(); + let ts: String = assistant_text.chars().take(400).collect(); + let fr2: Vec = file_reads.iter().take(5).cloned().collect(); + let fe2: Vec = file_edits.iter().take(5).cloned().collect(); + + let review_prompt = format!( + "你是蜂群审查员(书记官+纪律委员+提示蜂)。审查Agent本轮:\n\ + 工具:[{}] 读取:[{}] 编辑:[{}] 动作:{}\n\n\ + ## 书记官(上下文守护)\n\ + - 上下文压缩了?提取压缩前的关键决策/进度/用户纠正 → CTX:<要点>\n\ + - 轮次>20或token告急?预摘要关键状态 → CTX:<摘要>\n\ + - Agent忘记重要信息(重复提问/忽略决策)?→ CTX:<提醒>\n\n\ + ## 纪律委员(行为审查·仅提醒不拦截)\n\ + - 编辑前未Read?→ WARN:未读即改-<文件>\n\ + - 删除了LionHeart库文件?→ WARN:受保护路径-禁止删除\n\ + - LionHeart库读写操作正常,删除操作警告\n\ + - 同工具连续失败?→ WARN:策略死循环\n\ + - PS+中文+JSON?→ WARN:PS编码风险\n\ + - 全部工具失败?→ WARN:全部失败\n\ + - 改完不验证?→ WARN:未验证\n\n\ + ## 提示蜂(技能推荐)\n\ + - MiniApp相关但没加载miniapp-dev?→ SKILL:miniapp-dev\n\ + - 编码但没加载Pair-Programming?→ SKILL:Pair Programming\n\ + - 调研但没加载agent-reach?→ SKILL:agent-reach\n\n\ + 输出(每行一个):PASS / WARN:<问题> / CTX:<内容> / SKILL:<名称>\n\ + 禁止输出ABORT——审查蜂只有提醒权限,不拦截任何操作。\n\ + 一切正常只回复PASS。", + tl.join(","), fr2.join(","), fe2.join(","), ts + ); + + let sid2 = context.session_id.clone(); + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().expect("bee-review"); + rt.block_on(async move { + if let Ok(factory) = crate::infrastructure::ai::get_global_ai_client_factory().await { + if let Ok(client) = factory.get_client_resolved("primary").await { + let msgs = vec![bitfun_core_types::Message::user(review_prompt)]; + if let Ok(mut stream) = client.send_message_stream(msgs, None, None).await { + use futures::StreamExt; + let mut result = String::new(); + while let Some(Ok(chunk)) = stream.stream.next().await { + if let Some(t) = chunk.text { result.push_str(&t); } + } + let trimmed = result.trim().to_string(); + if !trimmed.is_empty() && trimmed != "PASS" { + crate::agentic::tools::post_call_hooks::push_review_result(&sid2, trimmed); + } + } + } + } + }); + }); + } + + } + // Track partial recovery reason from the last round if round_result.partial_recovery_reason.is_some() { last_partial_recovery_reason = round_result.partial_recovery_reason.clone(); diff --git a/src/crates/assembly/core/src/agentic/tools/post_call_hooks.rs b/src/crates/assembly/core/src/agentic/tools/post_call_hooks.rs index db411435d2..5aa8fbf9ad 100644 --- a/src/crates/assembly/core/src/agentic/tools/post_call_hooks.rs +++ b/src/crates/assembly/core/src/agentic/tools/post_call_hooks.rs @@ -3,13 +3,79 @@ //! The tool framework stays generic and calls this module after successful //! tool execution. Domain-specific hooks must keep their own gating inside the //! owning domain module. +//! +//! ## Hook Architecture (inspired by cc-haha / Claude Code hook system) +//! +//! cc-haha has 24 hook events + pre-tool / post-tool + exit-code protocol. +//! BitFun's current hook is post-call only, so we use session-level state +//! tracking to enforce cross-call invariants: +//! +//! - FILE_READ_TRACKER: records files read per session → Edit/Write/Delete +//! without prior Read yields Abort. +//! - STALE_TRACKER: repeated same-tool calls → Abort at threshold 3. +//! - LIONHEART_PATH_GUARD: Delete on LionHeart library paths → Abort. +//! +//! Future: pre-tool hooks (cc-haha `executePreToolHooks`) would let us block +//! before execution instead of aborting after the fact. use crate::agentic::deep_review::tool_measurement; +use crate::agentic::session::session_manager::SessionManager; use crate::agentic::tools::tool_context_runtime::ToolUseContext; use bitfun_agent_runtime::post_call_hooks::{ - run_successful_tool_post_call_hooks, SuccessfulToolPostCallHookExecutor, + run_stop_hooks, run_successful_tool_post_call_hooks, HookResult, StopHookAggregatedResult, + StopHookContext, StopHookExecutor, SuccessfulToolPostCallHookExecutor, ToolCallSummary, }; use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; + +// ── File Read Tracking (data collection for B01/C01 context) ──── + +/// Tracks which files have been read per session. +/// +/// When a Read call succeeds, the file path is recorded here. +/// When an Edit/Write/Delete call fires, we check whether the target +/// file was previously read in this session. If not → Abort. +static FILE_READ_TRACKER: std::sync::LazyLock>>> = + std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); + +/// Normalize a file path for consistent lookup in FILE_READ_TRACKER. +fn normalize_path(path: &str) -> String { + path.replace('\\', "/").trim_end_matches('/').to_lowercase() +} + +/// Record that a file was successfully read in this session. +fn record_file_read(session_id: &str, file_path: &str) { + if let Ok(mut tracker) = FILE_READ_TRACKER.lock() { + tracker + .entry(session_id.to_string()) + .or_default() + .insert(normalize_path(file_path)); + } +} + +/// Remove the file-read tracking entry for a given session. +#[allow(dead_code)] +pub(crate) fn remove_file_read_tracker_for_session(session_id: &str) { + if let Ok(mut tracker) = FILE_READ_TRACKER.lock() { + tracker.remove(session_id); + } +} + +// ── Bee-Review Buffer (cc-haha pattern: LLM result → inject next round) ── + +use std::sync::LazyLock; +static REVIEW_BUFFER: LazyLock>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +/// Push a review result for a session (called from execution_engine spawn). +pub(crate) fn push_review_result(session_id: &str, result: String) { + if let Ok(mut buf) = REVIEW_BUFFER.lock() { + buf.entry(session_id.to_string()).or_default().push(result); + } +} + +// ── Hook Executor ─────────────────────────────────────────────── struct CorePostCallHookExecutor; @@ -22,13 +88,103 @@ impl SuccessfulToolPostCallHookExecutor for CorePostCallHookExec ) { tool_measurement::maybe_record_shared_context_tool_use(tool_name, input, context); } + + fn behavior_guard( + &mut self, + tool_name: &str, + input: &Value, + context: &ToolUseContext, + ) -> HookResult { + let session_id = match &context.session_id { + Some(id) => id.as_str(), + None => return HookResult::Continue, + }; + + // ── Track file reads (data collection only, no enforcement) ── + if matches!(tool_name, "Read" | "read_file") { + if let Some(file_path) = input.get("file_path").and_then(Value::as_str) { + record_file_read(session_id, file_path); + } + } + + // All enforcement moved to B01+C01 async agent review. + HookResult::Continue + } +} + +impl StopHookExecutor for CorePostCallHookExecutor { + /// B01+C01 review is handled by async agent sessions spawned in + /// execution_engine via std::thread + coordinator.start_dialog_turn. + /// The stop hook is a pure trigger point — no synchronous checks. + fn context_guard(&mut self, _ctx: &StopHookContext) -> HookResult { + HookResult::Continue + } + + fn behavior_guard(&mut self, _ctx: &StopHookContext) -> HookResult { + HookResult::Continue + } } pub(crate) fn record_successful_tool_call( tool_name: &str, input: &Value, context: &ToolUseContext, -) { +) -> HookResult { + let mut executor = CorePostCallHookExecutor; + run_successful_tool_post_call_hooks(tool_name, input, context, &mut executor) +} + +/// Stop hook — injects self-review reminder at round boundaries. +/// Matches the goal system pattern: no separate sessions, no context pushing, +/// just a reminder injected for the main agent to self-review. +pub(crate) fn run_stop_hooks_for_round( + _session_manager: &Arc, + session_id: &str, + turn_id: &str, + round_index: u32, + tool_calls: Vec, + assistant_text: &str, + file_reads: Vec, + file_edits: Vec, + round_has_more: bool, +) -> StopHookAggregatedResult { + let ctx = StopHookContext { + session_id: session_id.to_string(), + turn_id: turn_id.to_string(), + round_index, + tool_calls, + assistant_text_summary: assistant_text.to_string(), + file_reads, + file_edits, + round_has_more, + }; let mut executor = CorePostCallHookExecutor; - run_successful_tool_post_call_hooks(tool_name, input, context, &mut executor); + let mut result = run_stop_hooks(&ctx, &mut executor); + + // ── Bee-review: drain pending LLM results (cc-haha pattern) ── + if let Ok(mut buf) = REVIEW_BUFFER.lock() { + if let Some(results) = buf.remove(ctx.session_id.as_str()) { + for r in results { + let trimmed = r.trim(); + if trimmed.is_empty() || trimmed == "PASS" { + continue; + } + if trimmed.starts_with("ABORT:") || trimmed.starts_with("ABORT:") { + // 审查蜂无拦截权限,ABORT 降级为 WARN + result.additional_contexts.push(format!("[审查员] 提醒: {}", &trimmed[6..].trim())); + } else if trimmed.starts_with("CTX:") || trimmed.starts_with("CTX:") { + result.additional_contexts.push(format!("[书记官] 上下文恢复: {}", &trimmed[4..].trim())); + } else if trimmed.starts_with("SKILL:") || trimmed.starts_with("SKILL:") { + result.additional_contexts.push(format!("[提示蜂] 推荐加载: {}", &trimmed[6..].trim())); + } else if trimmed.starts_with("WARN:") || trimmed.starts_with("WARN:") { + result.additional_contexts.push(format!("[审查员] 警告: {}", &trimmed[5..].trim())); + } else { + result.additional_contexts.push(format!("[审查员] {}", trimmed)); + } + } + } + } + + result } + diff --git a/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs b/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs index 7c993eb69c..5663d6b5d8 100644 --- a/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs +++ b/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs @@ -208,7 +208,17 @@ pub(crate) async fn call_with_tool_runtime_hooks( }; if result.is_ok() { - post_call_hooks::record_successful_tool_call(tool_name, input, context); + let hook_result = post_call_hooks::record_successful_tool_call(tool_name, input, context); + // 审查蜂只有提醒权限,不拦截任何工具调用 + if let bitfun_agent_runtime::post_call_hooks::HookResult::Abort { + reason, .. + } = hook_result + { + log::warn!( + "[ToolGuard] Hook suggested abort for tool '{}': {reason} — ignored (reminder-only mode)", + tool_name + ); + } } result diff --git a/src/crates/execution/agent-runtime/src/post_call_hooks.rs b/src/crates/execution/agent-runtime/src/post_call_hooks.rs index a8ae2ac199..fbb963afb2 100644 --- a/src/crates/execution/agent-runtime/src/post_call_hooks.rs +++ b/src/crates/execution/agent-runtime/src/post_call_hooks.rs @@ -1,20 +1,43 @@ -//! Portable post-call hook routing decisions. +//! Portable post-call and lifecycle hook routing decisions. +//! +//! Hooks are organized into two tiers: +//! +//! - **Per-tool hooks** (`SuccessfulToolPostCall`, `BehaviorGuard`): +//! fire after each successful tool call. Fine-grained, single-operation scope. +//! +//! - **Turn-level hooks** (`Stop`): +//! fire after each dialog round completes. Whole-round scope — can inspect +//! the cumulative effect of multiple tool calls in a single round. +//! +//! Inspired by cc-haha's Stop hook (used by `/goal` to evaluate progress +//! after every assistant turn) and the LionBuddy V10 supervisor chain +//! (Plan→Do→Check→Act with peer review after each stage). use serde_json::Value; use std::collections::{HashMap, HashSet}; use std::path::Path; -/// Hook categories that concrete runtime integrations may execute after a -/// successful tool call. +/// Hook categories that concrete runtime integrations may execute. +/// +/// `SuccessfulToolPostCall` / `BehaviorGuard` fire per-tool. +/// `Stop` fires per-round (after the assistant message and all tool +/// results for a round are collected). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum RuntimeHookKind { SuccessfulToolPostCall, DeepReviewSharedContextToolUse, + BehaviorGuard, + /// Fires after each dialog round completes (assistant message + tool results). + /// Carries round-level context including all tool calls in the round. + Stop, } -pub const fn successful_tool_post_call_hooks() -> [RuntimeHookKind; 1] { - [RuntimeHookKind::DeepReviewSharedContextToolUse] +pub const fn successful_tool_post_call_hooks() -> [RuntimeHookKind; 2] { + [ + RuntimeHookKind::DeepReviewSharedContextToolUse, + RuntimeHookKind::BehaviorGuard, + ] } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -26,6 +49,22 @@ pub enum RuntimeHookErrorPolicy { RecordWarning, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HookResult { + Continue, + Abort { + reason: String, + fix_instruction: String, + max_retries: u32, + }, +} + +impl HookResult { + pub fn is_abort(&self) -> bool { + matches!(self, HookResult::Abort { .. }) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct RuntimeHookPlan { id: String, @@ -151,6 +190,15 @@ pub trait SuccessfulToolPostCallHookExecutor { input: &Value, context: &C, ); + + fn behavior_guard( + &mut self, + _tool_name: &str, + _input: &Value, + _context: &C, + ) -> HookResult { + HookResult::Continue + } } pub fn run_successful_tool_post_call_hooks( @@ -158,7 +206,8 @@ pub fn run_successful_tool_post_call_hooks( input: &Value, context: &C, executor: &mut E, -) where +) -> HookResult +where E: SuccessfulToolPostCallHookExecutor, { for hook in successful_tool_post_call_hooks() { @@ -166,9 +215,122 @@ pub fn run_successful_tool_post_call_hooks( RuntimeHookKind::DeepReviewSharedContextToolUse => { executor.record_deep_review_shared_context_tool_use(tool_name, input, context); } + RuntimeHookKind::BehaviorGuard => { + let result = executor.behavior_guard(tool_name, input, context); + if result.is_abort() { + return result; + } + } RuntimeHookKind::SuccessfulToolPostCall => {} + RuntimeHookKind::Stop => { + // Stop hooks are handled by run_stop_hooks() at the round level, + // not by the per-tool post-call dispatch. + } + } + } + HookResult::Continue +} + +// ── Stop (round-level) hooks ──────────────────────────────────── + +/// Summary of a single tool call within a round, for Stop hook inspection. +#[derive(Debug, Clone)] +pub struct ToolCallSummary { + pub tool_name: String, + pub is_error: bool, + /// First 256 chars of serialized tool input, for hook inspection. + pub input_preview: String, +} + +/// Context passed to Stop hooks after each dialog round completes. +#[derive(Debug, Clone)] +pub struct StopHookContext { + pub session_id: String, + pub turn_id: String, + pub round_index: u32, + pub tool_calls: Vec, + pub assistant_text_summary: String, + pub file_reads: Vec, + pub file_edits: Vec, + pub round_has_more: bool, +} + +/// Executor for Stop (round-level) hooks. +/// +/// Implementations provide two handlers that mirror the B01/C01 dual-bee +/// pattern from LionBuddy V10: +/// +/// - `context_guard` (B01 提示蜂): checks whether the agent had sufficient +/// context to make good decisions. May inject supplemental knowledge. +/// - `behavior_guard` (C01 审查蜂): checks whether the agent violated any +/// iron rules during the round. +pub trait StopHookExecutor { + /// B01 提示蜂 — context completeness check. + fn context_guard(&mut self, _ctx: &StopHookContext) -> HookResult { + HookResult::Continue + } + + /// C01 审查蜂 — iron-rule violation check. + fn behavior_guard(&mut self, _ctx: &StopHookContext) -> HookResult { + HookResult::Continue + } +} + +/// Aggregate result from running all Stop hook handlers. +/// +/// Collects the first Abort (if any) and all additional context strings. +#[derive(Debug, Clone, Default)] +pub struct StopHookAggregatedResult { + pub abort: Option, + pub additional_contexts: Vec, +} + +impl StopHookAggregatedResult { + pub fn is_abort(&self) -> bool { + self.abort.as_ref().is_some_and(|r| r.is_abort()) + } +} + +/// Run B01 context_guard followed by C01 behavior_guard for a round. +/// +/// Returns the aggregated result. If behavior_guard returns Abort, the +/// caller should inject the abort message into the next round. +pub fn run_stop_hooks( + ctx: &StopHookContext, + executor: &mut E, +) -> StopHookAggregatedResult { + let mut aggregated = StopHookAggregatedResult::default(); + + // B01 提示蜂: context completeness check (informational, non-blocking) + match executor.context_guard(ctx) { + HookResult::Continue => {} + HookResult::Abort { + reason, + fix_instruction, + .. + } => { + aggregated.additional_contexts.push(format!( + "[B01 提示蜂] 上下文不足: {reason} — {fix_instruction}" + )); } } + + // C01 审查蜂: iron-rule violation check (提醒模式,不拦截) + let c01 = executor.behavior_guard(ctx); + if c01.is_abort() { + if let HookResult::Abort { + reason, + fix_instruction, + .. + } = c01 + { + aggregated.additional_contexts.push(format!( + "[C01 审查蜂] 提醒: {reason} — {fix_instruction}" + )); + } + } + + aggregated } #[derive(Debug, Clone, Copy)] diff --git a/src/crates/execution/agent-runtime/src/session_control.rs b/src/crates/execution/agent-runtime/src/session_control.rs index bf0e4e44b7..532d518b96 100644 --- a/src/crates/execution/agent-runtime/src/session_control.rs +++ b/src/crates/execution/agent-runtime/src/session_control.rs @@ -24,23 +24,12 @@ impl SessionControlAction { } } -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -pub enum SessionControlAgentType { - #[serde(rename = "agentic", alias = "Agentic", alias = "AGENTIC")] - Agentic, - #[serde(rename = "Plan", alias = "plan", alias = "PLAN")] - Plan, - #[serde(rename = "Cowork", alias = "cowork", alias = "COWORK")] - Cowork, -} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionControlAgentType(pub String); impl SessionControlAgentType { - pub const fn as_str(&self) -> &'static str { - match self { - Self::Agentic => "agentic", - Self::Plan => "Plan", - Self::Cowork => "Cowork", - } + pub fn as_str(&self) -> &str { + &self.0 } } diff --git a/src/crates/execution/agent-runtime/tests/post_call_hook_execution_contracts.rs b/src/crates/execution/agent-runtime/tests/post_call_hook_execution_contracts.rs index 48dfa0b84b..39ef9554f9 100644 --- a/src/crates/execution/agent-runtime/tests/post_call_hook_execution_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/post_call_hook_execution_contracts.rs @@ -1,6 +1,6 @@ use bitfun_agent_runtime::post_call_hooks::{ resolve_deep_review_shared_context_tool_use, run_successful_tool_post_call_hooks, - DeepReviewSharedContextToolUseFacts, SuccessfulToolPostCallHookExecutor, + DeepReviewSharedContextToolUseFacts, HookResult, SuccessfulToolPostCallHookExecutor, }; use serde_json::{json, Value}; use std::collections::HashMap; @@ -21,6 +21,15 @@ impl SuccessfulToolPostCallHookExecutor<&str> for RecordingExecutor { self.calls .push((tool_name.to_string(), input.clone(), (*context).to_string())); } + + fn behavior_guard( + &mut self, + _tool_name: &str, + _input: &Value, + _context: &&str, + ) -> HookResult { + HookResult::Continue + } } #[test] diff --git a/src/crates/execution/agent-runtime/tests/session_control_contracts.rs b/src/crates/execution/agent-runtime/tests/session_control_contracts.rs index 57ebaf23b5..bdb698f99e 100644 --- a/src/crates/execution/agent-runtime/tests/session_control_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/session_control_contracts.rs @@ -51,7 +51,7 @@ fn rejects_current_session_mutation_when_context_matches() { fn validates_create_requires_workspace_and_creator_session() { let mut input = base_input(SessionControlAction::Create); input.workspace = Some(std::env::temp_dir().to_string_lossy().to_string()); - input.agent_type = Some(SessionControlAgentType::Plan); + input.agent_type = Some(SessionControlAgentType("Plan".to_string())); let missing_creator = validate_session_control_input(&input, SessionControlValidationContext::default()); diff --git a/src/crates/services/services-integrations/src/mcp/protocol/client_info.rs b/src/crates/services/services-integrations/src/mcp/protocol/client_info.rs index 157ca7d3c1..6abed44a1e 100644 --- a/src/crates/services/services-integrations/src/mcp/protocol/client_info.rs +++ b/src/crates/services/services-integrations/src/mcp/protocol/client_info.rs @@ -8,8 +8,6 @@ pub fn create_mcp_client_info( ) -> ClientInfo { ClientInfo::new( ClientCapabilities::builder() - .enable_roots() - .enable_sampling() .enable_elicitation() .build(), Implementation::new(client_name, client_version), diff --git a/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs b/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs index 77e984a27e..faa0a13bec 100644 --- a/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs +++ b/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs @@ -202,7 +202,7 @@ impl StreamableHttpClient for BitFunStreamableHttpClient { } } - let event_stream = SseStream::from_byte_stream(response.bytes_stream()).boxed(); + let event_stream = SseStream::from_bytes_stream(response.bytes_stream()).boxed(); Ok(event_stream) } @@ -303,7 +303,7 @@ impl StreamableHttpClient for BitFunStreamableHttpClient { match content_type.as_deref() { Some(ct) if ct.as_bytes().starts_with(EVENT_STREAM_MIME_TYPE.as_bytes()) => { - let event_stream = SseStream::from_byte_stream(response.bytes_stream()).boxed(); + let event_stream = SseStream::from_bytes_stream(response.bytes_stream()).boxed(); Ok(StreamableHttpPostResponse::Sse(event_stream, session_id)) } Some(ct) if ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes()) => {