llm: add dynamic tool loading (DTL) — Kimi system-tools + Anthropic tool-reference
Nightly Build / build (push) Successful in 6m51s
Nightly Build / build (push) Successful in 6m51s
Replace the old session_mcp_grants/stack_mcp_grants table pair with
a single activated_tools table that anchors each activation at the
assistant message_id that triggered it. The durable write moves from
the activate_tools tool itself to the round loop (handle_tool_call),
which has the message_id the DTL serializer positions injected tool
blocks against.
Introduce DtlMode (None / AnthropicToolReference / KimiSystemTools),
resolved per model from capabilities (opt-in via tool_search)
combined with the provider's dtl_format(). The message builder inserts
Kimi system {tools} blocks at the activation position, or emits
Anthropic tool_reference markers on the tool result. The tool-def
surface (all_tool_defs) switches shape: Anthropic declares everything
deferred; Kimi omits activated tools from the top-level array (system
takes over); None keeps the old grant-set logic.
Anthropic client: accept structured system arrays (cache_control on
the static block when DTL is active), carry defer_loading through
conversion, emit tool_reference blocks on result messages. Prompt
caching enabled exactly when DTL is active (anthropic provider).
MCP server list in the prompt is now a static catalogue (not split
Available/Active) — the split invalidated the cache on every activation.
Groundwork for providers.yaml dtl: key; Moonshot/Kimi providers wired
with kimi_system_tools and the k3* enrich now adds tool_search.
Compactor re-anchors activations whose message was compacted away.
This commit is contained in:
@@ -6,7 +6,7 @@ use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::info;
|
||||
|
||||
use crate::db::{chat_history, chat_llm_tools, chat_sessions_stack, scratchpad, stack_mcp_grants};
|
||||
use crate::db::{activated_tools, chat_history, chat_llm_tools, chat_sessions_stack, scratchpad};
|
||||
use crate::events::ServerEvent;
|
||||
|
||||
use super::{ChatSessionHandler, MAX_AGENT_DEPTH, TurnOutcome};
|
||||
@@ -108,8 +108,8 @@ impl ChatSessionHandler {
|
||||
// Sub-agents never inject live user input.
|
||||
let outcome = self.run_agent_turn(child.id, &child_config, token, tx, None).await;
|
||||
|
||||
if let Err(e) = stack_mcp_grants::delete_for_stack(pool, child.id).await {
|
||||
tracing::warn!(stack_id = child.id, error = %e, "dispatch_sub_agent: failed to delete stack MCP grants");
|
||||
if let Err(e) = activated_tools::delete_for_stack(pool, child.id).await {
|
||||
tracing::warn!(stack_id = child.id, error = %e, "dispatch_sub_agent: failed to delete stack activations");
|
||||
}
|
||||
|
||||
let parent_agent_id = parent_config.agent_id.clone();
|
||||
@@ -165,7 +165,7 @@ impl ChatSessionHandler {
|
||||
stack_id: i64,
|
||||
depth: i64,
|
||||
) -> anyhow::Result<AgentRunConfig> {
|
||||
let persisted_grants = stack_mcp_grants::list_for_stack(&self.db, stack_id)
|
||||
let persisted_grants = activated_tools::list_refs_stack(&self.db, stack_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let active_mcp_grants: Arc<RwLock<HashSet<String>>> =
|
||||
@@ -205,8 +205,6 @@ impl ChatSessionHandler {
|
||||
|
||||
{
|
||||
let activate_tool = crate::tools::activate_tools::ActivateTools {
|
||||
pool: Arc::clone(&self.db),
|
||||
session_id: self.session_id,
|
||||
stack_id: Some(stack_id),
|
||||
mcp: Arc::clone(&self.mcp),
|
||||
active_mcp_grants: Arc::clone(&active_mcp_grants),
|
||||
|
||||
@@ -140,7 +140,7 @@ impl ChatSessionHandler {
|
||||
// Load persisted session grants from DB (MCP server names and/or the reserved
|
||||
// `config` keyword), then inject `activate_tools` so the LLM can activate
|
||||
// additional groups on demand.
|
||||
let persisted = crate::db::session_mcp_grants::list_for_session(
|
||||
let persisted = crate::db::activated_tools::list_refs_session(
|
||||
&self.db, self.session_id,
|
||||
).await.unwrap_or_default();
|
||||
|
||||
@@ -148,14 +148,10 @@ impl ChatSessionHandler {
|
||||
Arc::new(RwLock::new(persisted.into_iter().collect()));
|
||||
|
||||
{
|
||||
let pool_clone = Arc::clone(&self.db);
|
||||
let session_id = self.session_id;
|
||||
let mcp_clone = Arc::clone(&self.mcp);
|
||||
let grants_clone = Arc::clone(&active_mcp_grants);
|
||||
|
||||
let activate_tool = crate::tools::activate_tools::ActivateTools {
|
||||
pool: pool_clone,
|
||||
session_id,
|
||||
stack_id: None,
|
||||
mcp: mcp_clone,
|
||||
active_mcp_grants: grants_clone,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::{Arc, RwLock};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::llm::DtlMode;
|
||||
use crate::mcp::McpProvider;
|
||||
use crate::tools::Tool;
|
||||
use crate::tools::tool_names as tn;
|
||||
@@ -56,10 +57,10 @@ pub struct AgentRunConfig {
|
||||
pub mcp: Arc<dyn McpProvider>,
|
||||
/// Set of MCP server names currently granted (activated) for this agent run.
|
||||
///
|
||||
/// - Root agents: pre-populated from `session_mcp_grants` DB at config-build time;
|
||||
/// updated in-place by `activate_tools`.
|
||||
/// - Root agents: pre-populated from the `activated_tools` table (session-scoped
|
||||
/// rows) at config-build time; updated in-place by `activate_tools`.
|
||||
/// - Sub-agents: starts empty; populated by `activate_tools` (stack-scoped, no
|
||||
/// session leak); deleted from DB when the stack frame terminates.
|
||||
/// session leak); the frame's rows are deleted when the stack frame terminates.
|
||||
///
|
||||
/// May also contain the reserved keyword `"config"`, which unlocks the built-in
|
||||
/// `Config`-category tools (`config_tool_defs`) rather than an MCP server.
|
||||
@@ -79,31 +80,39 @@ impl AgentRunConfig {
|
||||
///
|
||||
/// Dynamic groups are re-queried every call so that an `activate_tools` call in
|
||||
/// round N makes the tools visible in round N+1 without rebuilding the whole config.
|
||||
pub fn all_tool_defs(&self) -> Vec<Value> {
|
||||
pub fn all_tool_defs(&self, dtl: DtlMode) -> Vec<Value> {
|
||||
let mut defs = self.base_tool_defs.clone();
|
||||
|
||||
// Dynamic groups: read the currently-granted set (MCP server names + `config`).
|
||||
let granted: HashSet<String> = self.active_mcp_grants
|
||||
.read()
|
||||
.map(|g| g.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// MCP servers: include tools for the granted server names.
|
||||
let servers: Vec<String> = granted.iter()
|
||||
.filter(|n| n.as_str() != crate::tools::tool_names::CONFIG_GROUP)
|
||||
.cloned()
|
||||
.collect();
|
||||
if !servers.is_empty() {
|
||||
defs.extend(
|
||||
self.mcp.tools_for(&servers)
|
||||
.iter()
|
||||
.map(|t| t.to_openai_definition()),
|
||||
);
|
||||
}
|
||||
|
||||
// `config` group: include the built-in Config-category tools on demand.
|
||||
if granted.contains(crate::tools::tool_names::CONFIG_GROUP) {
|
||||
defs.extend(self.config_tool_defs.iter().cloned());
|
||||
match dtl {
|
||||
// Anthropic custom tool_reference: declare EVERY accessible MCP tool +
|
||||
// the config group as `defer_loading:true`, on every turn. The toolset
|
||||
// is stable (cache-safe) and `activate_tools` loads the needed ones via
|
||||
// tool_reference. Deferred defs are excluded from the prompt prefix by
|
||||
// the API and cost nothing until referenced.
|
||||
DtlMode::AnthropicToolReference => {
|
||||
defs.extend(self.mcp.tools().iter().map(|t| deferred(t.to_openai_definition())));
|
||||
defs.extend(self.config_tool_defs.iter().cloned().map(deferred));
|
||||
}
|
||||
// Kimi K3: activated MCP/config tools are injected as `system` messages
|
||||
// by the message builder, so they are NOT in the top-level tools here.
|
||||
DtlMode::KimiSystemTools => {}
|
||||
// Today's behaviour: only the currently-granted MCP servers + config.
|
||||
DtlMode::None => {
|
||||
let granted: HashSet<String> = self.active_mcp_grants
|
||||
.read()
|
||||
.map(|g| g.clone())
|
||||
.unwrap_or_default();
|
||||
let servers: Vec<String> = granted.iter()
|
||||
.filter(|n| n.as_str() != crate::tools::tool_names::CONFIG_GROUP)
|
||||
.cloned()
|
||||
.collect();
|
||||
if !servers.is_empty() {
|
||||
defs.extend(self.mcp.tools_for(&servers).iter().map(|t| t.to_openai_definition()));
|
||||
}
|
||||
if granted.contains(crate::tools::tool_names::CONFIG_GROUP) {
|
||||
defs.extend(self.config_tool_defs.iter().cloned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defs.extend(self.memory_tools.iter().map(|t| t.openai_definition()));
|
||||
@@ -167,3 +176,12 @@ impl AgentRunConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tags an OpenAI tool definition as deferred (Anthropic tool search): the API
|
||||
/// keeps it out of the prompt prefix until `activate_tools` references it. The
|
||||
/// flag rides on the top-level tool object; `AnthropicClient::convert_tools`
|
||||
/// maps it to Anthropic's native `defer_loading` field.
|
||||
fn deferred(mut def: Value) -> Value {
|
||||
def["defer_loading"] = Value::Bool(true);
|
||||
def
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ impl ChatSessionHandler {
|
||||
stack_id: i64,
|
||||
config: &AgentRunConfig,
|
||||
active_grants: &HashSet<String>,
|
||||
tool_defs: &[Value],
|
||||
req_scope: Option<&str>,
|
||||
req_strength: Option<LlmStrength>,
|
||||
cur_name: &mut String,
|
||||
@@ -57,6 +56,9 @@ impl ChatSessionHandler {
|
||||
let mut tried_this_round: Vec<String> = vec![cur_name.clone()];
|
||||
|
||||
loop {
|
||||
// Re-derive the tool defs for the model actually serving this attempt:
|
||||
// a fallback across DTL modes must re-shape (deferred candidates or not).
|
||||
let cur_tool_defs = config.all_tool_defs(cur_llm.dtl);
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
let options = ChatOptions {
|
||||
model: cur_llm.model.clone(),
|
||||
@@ -72,8 +74,8 @@ impl ChatSessionHandler {
|
||||
// open directly — keyed on the model actually serving this attempt, so a
|
||||
// fallback to a text-only model drops the claim. `None` (no media
|
||||
// capability) leaves the shared defs untouched, avoiding a clone.
|
||||
let annotated = media_annotated_tools(tool_defs, &cur_llm.capabilities);
|
||||
let defs: &[Value] = annotated.as_deref().unwrap_or(tool_defs);
|
||||
let annotated = media_annotated_tools(&cur_tool_defs, &cur_llm.capabilities);
|
||||
let defs: &[Value] = annotated.as_deref().unwrap_or(&cur_tool_defs);
|
||||
|
||||
// Clone the Arc so the in-flight future does not borrow `cur_llm` across
|
||||
// the fallback reassignment below. On cancel we drop the future
|
||||
@@ -164,11 +166,13 @@ impl ChatSessionHandler {
|
||||
// settings (e.g. switching from OpenRouter/Anthropic to DeepSeek)
|
||||
// or different input capabilities (a non-vision fallback drops
|
||||
// inline media back to the textual path block).
|
||||
let activation_stack = if config.depth == 0 { None } else { Some(stack_id) };
|
||||
match self.build_openai_messages(
|
||||
&self.db, stack_id, &config.agent_id,
|
||||
config.extra_system.as_deref(), config.extra_system_dynamic.as_deref(),
|
||||
config.tail_reminder.as_deref(), active_grants,
|
||||
&config.system_substitutions, cur_llm.prompt_cache, &cur_llm.capabilities,
|
||||
cur_llm.dtl, &config.config_tool_defs, activation_stack,
|
||||
).await {
|
||||
Ok(m) => *messages = m,
|
||||
Err(e) => return RoundLlm::Failed(e),
|
||||
|
||||
@@ -117,8 +117,11 @@ impl ChatSessionHandler {
|
||||
// Messages are (re)built with the current model's prompt_cache flag.
|
||||
// On fallback within the same round `call_llm_round` rebuilds them again
|
||||
// if the replacement model has a different prompt_cache setting.
|
||||
let mut messages = self.build_openai_messages(pool, stack_id, &config.agent_id, config.extra_system.as_deref(), config.extra_system_dynamic.as_deref(), config.tail_reminder.as_deref(), &active_grants_snapshot, &config.system_substitutions, cur_llm.prompt_cache, &cur_llm.capabilities).await?;
|
||||
let tool_defs = config.all_tool_defs();
|
||||
// Activation scope for the DTL serializer: session-scoped for the root
|
||||
// agent (stack_id NULL), the frame itself for a sub-agent.
|
||||
let activation_stack = if config.depth == 0 { None } else { Some(stack_id) };
|
||||
let mut messages = self.build_openai_messages(pool, stack_id, &config.agent_id, config.extra_system.as_deref(), config.extra_system_dynamic.as_deref(), config.tail_reminder.as_deref(), &active_grants_snapshot, &config.system_substitutions, cur_llm.prompt_cache, &cur_llm.capabilities, cur_llm.dtl, &config.config_tool_defs, activation_stack).await?;
|
||||
let tool_defs = config.all_tool_defs(cur_llm.dtl);
|
||||
|
||||
// Record every tool actually offered to the LLM so the Security-groups
|
||||
// UI can list/gate dynamically-injected tools. Cheap no-op once each
|
||||
@@ -128,7 +131,7 @@ impl ChatSessionHandler {
|
||||
// One LLM call for this round, with automatic model fallback on
|
||||
// retriable errors. `cur_name`/`cur_llm`/`messages` are updated in place.
|
||||
let turn_result = match self.call_llm_round(
|
||||
stack_id, config, &active_grants_snapshot, &tool_defs,
|
||||
stack_id, config, &active_grants_snapshot,
|
||||
req_scope.as_deref(), req_strength,
|
||||
&mut cur_name, &mut cur_llm, &mut messages, token, &em,
|
||||
).await {
|
||||
@@ -274,6 +277,25 @@ impl ChatSessionHandler {
|
||||
DispatchResult::AbortPending => return Ok(CallFlow::End(TurnOutcome::Cancelled)),
|
||||
};
|
||||
|
||||
// Persist the durable effect of `activate_tools`, anchored at the assistant
|
||||
// `message_id` that triggered it (the anchor the DTL serializer positions
|
||||
// injected tool blocks against). The in-memory grant set was already updated
|
||||
// inside the tool; this records it across turns and restarts.
|
||||
if call.name == crate::tools::tool_names::ACTIVATE_TOOLS {
|
||||
if let Some(groups) = call.arguments.get("groups").and_then(|g| g.as_array()) {
|
||||
// Root (depth 0) → session-scoped (stack_id NULL); sub-agent → its frame.
|
||||
let anchor_stack = if config.depth == 0 { None } else { Some(stack_id) };
|
||||
for g in groups.iter().filter_map(|v| v.as_str()) {
|
||||
let kind = if g == crate::tools::tool_names::CONFIG_GROUP { "builtin" } else { "mcp" };
|
||||
if let Err(e) = crate::db::activated_tools::grant(
|
||||
pool, self.session_id, anchor_stack, message_id, kind, g,
|
||||
).await {
|
||||
tracing::warn!(session_id = self.session_id, group = g, error = %e, "activate_tools: failed to persist activation");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.record_tool_outcome(
|
||||
tool_call_id, &call.name, &call.arguments, outcome, preview, em, Some(all_tool_calls),
|
||||
).await? {
|
||||
|
||||
@@ -10,6 +10,7 @@ use core_api::user_fs::UserFs;
|
||||
use crate::compactor::{ContextCompactor, SUMMARY_PREFIX};
|
||||
use crate::config::DatetimeConfig;
|
||||
use crate::db::{chat_history, chat_llm_tools, chat_summaries};
|
||||
use crate::llm::DtlMode;
|
||||
use crate::mcp::McpProvider;
|
||||
use crate::tools::tool_names as tn;
|
||||
|
||||
@@ -108,6 +109,12 @@ impl MessageBuilder {
|
||||
// Input capabilities of the resolved model (`vision`, `video`, …) —
|
||||
// drives inline media for current-turn attachments.
|
||||
capabilities: &[String],
|
||||
// Dynamic-tool-loading mode for the resolved model, plus the config-group
|
||||
// tool defs (needed to resolve activated tools). `activation_stack` scopes
|
||||
// the activation read: `None` = session-scoped (root), `Some(id)` = frame.
|
||||
dtl: DtlMode,
|
||||
config_tool_defs: &[Value],
|
||||
activation_stack: Option<i64>,
|
||||
) -> anyhow::Result<Vec<Value>> {
|
||||
let pool = &*self.pool;
|
||||
|
||||
@@ -254,6 +261,15 @@ impl MessageBuilder {
|
||||
media_turn_start -= 1;
|
||||
}
|
||||
|
||||
// DTL: resolve the tools activated at each assistant message (empty unless a
|
||||
// DTL mode is active). Drives the Kimi `system`+`tools` injection and the
|
||||
// Anthropic `tool_reference` markers emitted in the history loop below.
|
||||
let activation_defs: HashMap<i64, Vec<Value>> = if matches!(dtl, DtlMode::None) {
|
||||
HashMap::new()
|
||||
} else {
|
||||
self.resolve_activation_defs(activation_stack, config_tool_defs).await
|
||||
};
|
||||
|
||||
for (idx, entry) in history.iter().enumerate() {
|
||||
let is_previous_turn = current_turn_boundary.is_some_and(|b| idx < b);
|
||||
|
||||
@@ -378,11 +394,29 @@ impl MessageBuilder {
|
||||
tc.arguments.as_deref(),
|
||||
);
|
||||
|
||||
out.push(json!({
|
||||
let mut tool_msg = json!({
|
||||
"role": "tool",
|
||||
"tool_call_id": format!("tc_{}", tc.id),
|
||||
"content": result_content,
|
||||
}));
|
||||
});
|
||||
// Anthropic DTL: an `activate_tools` result becomes a set of
|
||||
// `tool_reference`s (the activated groups' tool names) that the
|
||||
// client renders as tool_reference blocks and the API expands
|
||||
// into the deferred tools. `_tool_references` is a neutral
|
||||
// marker other clients ignore.
|
||||
if matches!(dtl, DtlMode::AnthropicToolReference)
|
||||
&& tc.name == tn::ACTIVATE_TOOLS
|
||||
&& let Some(adefs) = activation_defs.get(&entry.id)
|
||||
{
|
||||
let names: Vec<Value> = adefs.iter()
|
||||
.filter_map(|d| d["function"]["name"].as_str())
|
||||
.map(|n| Value::String(n.to_string()))
|
||||
.collect();
|
||||
if !names.is_empty() {
|
||||
tool_msg["_tool_references"] = Value::Array(names);
|
||||
}
|
||||
}
|
||||
out.push(tool_msg);
|
||||
}
|
||||
|
||||
// Media a tool produced this turn (e.g. read_file on an
|
||||
@@ -411,6 +445,16 @@ impl MessageBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kimi K3 DTL: inject the tools activated at this assistant
|
||||
// message as a `system` message carrying a `tools` field, right
|
||||
// after its tool-result group (append-only → cache-safe).
|
||||
if matches!(dtl, DtlMode::KimiSystemTools)
|
||||
&& let Some(adefs) = activation_defs.get(&entry.id)
|
||||
&& !adefs.is_empty()
|
||||
{
|
||||
out.push(json!({ "role": "system", "tools": adefs }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -585,7 +629,49 @@ impl MessageBuilder {
|
||||
))
|
||||
}
|
||||
|
||||
fn render_mcp_list(&self, active_mcp_grants: &HashSet<String>) -> String {
|
||||
/// Resolves the tools activated at each assistant message, for DTL
|
||||
/// serialization. Returns `message_id → deduped OpenAI tool defs`. MCP groups
|
||||
/// resolve to the server's live tool defs; the `config` group resolves to the
|
||||
/// passed-in config-category defs. Scope mirrors the in-memory grant set:
|
||||
/// `None` = session-scoped (root), `Some(id)` = the sub-agent frame.
|
||||
async fn resolve_activation_defs(
|
||||
&self,
|
||||
activation_stack: Option<i64>,
|
||||
config_tool_defs: &[Value],
|
||||
) -> HashMap<i64, Vec<Value>> {
|
||||
let activations = crate::db::activated_tools::list_active_at(
|
||||
&self.pool, self.session_id, activation_stack, i64::MAX,
|
||||
).await.unwrap_or_default();
|
||||
|
||||
let mut map: HashMap<i64, Vec<Value>> = HashMap::new();
|
||||
for a in activations {
|
||||
let resolved: Vec<Value> = match a.kind.as_str() {
|
||||
"mcp" => self.mcp.tools_for(&[a.ref_.clone()])
|
||||
.iter().map(|t| t.to_openai_definition()).collect(),
|
||||
"builtin" if a.ref_ == tn::CONFIG_GROUP => config_tool_defs.to_vec(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let entry = map.entry(a.message_id).or_default();
|
||||
for d in resolved {
|
||||
if let Some(name) = d["function"]["name"].as_str().map(str::to_string) {
|
||||
if !entry.iter().any(|e| e["function"]["name"].as_str() == Some(name.as_str())) {
|
||||
entry.push(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// A **static** catalogue of the MCP servers this user can load — identical
|
||||
/// regardless of which are currently active. Static is deliberate: this text
|
||||
/// sits inside the `cache_control: ephemeral` system block, so the old
|
||||
/// Available/Active split (which moved a server between tables on activation)
|
||||
/// invalidated the prompt-cache prefix on every `activate_tools` call. Which
|
||||
/// servers are active is already visible to the model through the injected
|
||||
/// tools. The grant set is retained in the signature only for call-site
|
||||
/// stability (the DTL serializer may consume it later).
|
||||
fn render_mcp_list(&self, _active_mcp_grants: &HashSet<String>) -> String {
|
||||
let all_servers: std::collections::BTreeSet<String> = self.mcp.tools()
|
||||
.into_iter()
|
||||
.map(|t| t.server_name)
|
||||
@@ -597,37 +683,17 @@ impl MessageBuilder {
|
||||
|
||||
let descriptions = self.mcp.server_descriptions();
|
||||
|
||||
let hidden: Vec<&String> = all_servers.iter()
|
||||
.filter(|n| !active_mcp_grants.contains(*n))
|
||||
.collect();
|
||||
let active: Vec<&String> = all_servers.iter()
|
||||
.filter(|n| active_mcp_grants.contains(*n))
|
||||
.collect();
|
||||
|
||||
let mut out = String::from("## MCP servers\n");
|
||||
|
||||
if !hidden.is_empty() {
|
||||
out.push_str("\n**Available** — call `activate_tools([\"name\"])` to load tools:\n\n");
|
||||
out.push_str("| Server | Description |\n|--------|-------------|\n");
|
||||
for name in &hidden {
|
||||
let desc = descriptions.get(*name)
|
||||
.and_then(|d| d.as_deref())
|
||||
.unwrap_or("—");
|
||||
out.push_str(&format!("| `{name}` | {desc} |\n"));
|
||||
}
|
||||
let mut out = String::from(
|
||||
"## MCP servers\n\nConnectors you can load with `activate_tools([\"name\"])`. \
|
||||
Once loaded, a server's tools are callable as `mcp__<name>__<tool>`:\n\n",
|
||||
);
|
||||
out.push_str("| Server | Description |\n|--------|-------------|\n");
|
||||
for name in &all_servers {
|
||||
let desc = descriptions.get(name)
|
||||
.and_then(|d| d.as_deref())
|
||||
.unwrap_or("—");
|
||||
out.push_str(&format!("| `{name}` | {desc} |\n"));
|
||||
}
|
||||
|
||||
if !active.is_empty() {
|
||||
out.push_str("\n**Active** — tools callable as `mcp__<name>__<tool>`:\n\n");
|
||||
out.push_str("| Server | Description |\n|--------|-------------|\n");
|
||||
for name in &active {
|
||||
let desc = descriptions.get(*name)
|
||||
.and_then(|d| d.as_deref())
|
||||
.unwrap_or("—");
|
||||
out.push_str(&format!("| `{name}` | {desc} |\n"));
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::llm::DtlMode;
|
||||
use super::ChatSessionHandler;
|
||||
use super::message_builder::MessageBuilder;
|
||||
|
||||
@@ -23,6 +24,9 @@ impl ChatSessionHandler {
|
||||
system_substitutions: &HashMap<String, String>,
|
||||
cache_hints: bool,
|
||||
capabilities: &[String],
|
||||
dtl: DtlMode,
|
||||
config_tool_defs: &[Value],
|
||||
activation_stack: Option<i64>,
|
||||
) -> anyhow::Result<Vec<Value>> {
|
||||
let project_root = self.run_context.read().await
|
||||
.as_ref()
|
||||
@@ -45,6 +49,6 @@ impl ChatSessionHandler {
|
||||
// `pool` is passed in from the caller (always `&self.db`) but we take
|
||||
// ownership via Arc::clone above so the signature stays backward-compatible.
|
||||
let _ = pool; // suppress unused-variable warning; MessageBuilder uses its own Arc
|
||||
builder.build(stack_id, agent_id, extra_system_static, extra_system_dynamic, tail_reminder, active_mcp_grants, system_substitutions, cache_hints, capabilities).await
|
||||
builder.build(stack_id, agent_id, extra_system_static, extra_system_dynamic, tail_reminder, active_mcp_grants, system_substitutions, cache_hints, capabilities, dtl, config_tool_defs, activation_stack).await
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user