наблюдаемость: измерять бюджет каталога MCP
This commit is contained in:
@@ -8,7 +8,9 @@ use crank_core::{CacheScope, CoordinationStateStore, CoordinationStateValue};
|
||||
use crank_registry::{PostgresRegistry, PublishedAgentTool, RegistryError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::info;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::manifest::analyze_published_tool_catalog;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PublishedToolCatalog {
|
||||
@@ -105,6 +107,7 @@ impl PublishedToolCatalog {
|
||||
}
|
||||
|
||||
if let Some((tools, age)) = self.load_shared_snapshot(workspace_slug, agent_slug).await {
|
||||
log_catalog_analysis(workspace_slug, agent_slug, "shared_cache", &tools);
|
||||
let mut guard = self.cached.write().await;
|
||||
guard.insert(
|
||||
key,
|
||||
@@ -125,6 +128,7 @@ impl PublishedToolCatalog {
|
||||
Err(RegistryError::PublishedAgentNotFound { .. }) => Vec::new(),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
log_catalog_analysis(workspace_slug, agent_slug, "postgres", &tools);
|
||||
self.store_shared_snapshot(workspace_slug, agent_slug, &tools)
|
||||
.await;
|
||||
let mut guard = self.cached.write().await;
|
||||
@@ -208,6 +212,42 @@ impl PublishedToolCatalog {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_catalog_analysis(
|
||||
workspace_slug: &str,
|
||||
agent_slug: &str,
|
||||
source: &str,
|
||||
tools: &[PublishedAgentTool],
|
||||
) {
|
||||
let analysis = match analyze_published_tool_catalog(tools) {
|
||||
Ok(analysis) => analysis,
|
||||
Err(error) => {
|
||||
warn!(workspace_slug, agent_slug, source, %error, "published catalog analysis failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let warning_count = analysis
|
||||
.quality
|
||||
.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.severity == crank_core::ToolQualitySeverity::Warning)
|
||||
.count();
|
||||
|
||||
info!(
|
||||
workspace_slug,
|
||||
agent_slug,
|
||||
source,
|
||||
tool_count = analysis.budget.tool_count,
|
||||
serialized_bytes = analysis.budget.serialized_bytes,
|
||||
estimated_context_tokens = analysis.budget.estimated_context_tokens,
|
||||
largest_tool_estimated_context_tokens =
|
||||
analysis.budget.largest_tool_estimated_context_tokens,
|
||||
recommended_context_tokens = analysis.budget.recommended_context_tokens,
|
||||
exceeds_recommended_budget = analysis.budget.exceeds_recommended_budget,
|
||||
catalog_quality_warning_count = warning_count,
|
||||
"published agent catalog analyzed"
|
||||
);
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crank_core::{HttpMethod, OperationSafetyClass, OperationSafetyPolicy, Target};
|
||||
use crank_core::{
|
||||
HttpMethod, OperationSafetyClass, OperationSafetyPolicy, Target, ToolCatalogAnalysis,
|
||||
ToolCatalogAnalysisError, ToolQualityCatalogTool, analyze_tool_catalog,
|
||||
};
|
||||
use crank_registry::PublishedAgentTool;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -27,6 +30,37 @@ pub fn tool_definitions(tool: &PublishedAgentTool) -> Vec<Value> {
|
||||
)]
|
||||
}
|
||||
|
||||
pub fn analyze_published_tool_catalog(
|
||||
tools: &[PublishedAgentTool],
|
||||
) -> Result<ToolCatalogAnalysis, ToolCatalogAnalysisError> {
|
||||
let mut quality_tools = Vec::new();
|
||||
let mut definitions = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
for definition in tool_definitions(tool) {
|
||||
quality_tools.push(ToolQualityCatalogTool {
|
||||
name: definition_string(&definition, "name")?,
|
||||
display_name: definition_string(&definition, "title")?,
|
||||
description: definition_string(&definition, "description")?,
|
||||
});
|
||||
definitions.push(definition);
|
||||
}
|
||||
}
|
||||
|
||||
analyze_tool_catalog(&quality_tools, &definitions)
|
||||
}
|
||||
|
||||
fn definition_string(
|
||||
definition: &Value,
|
||||
field: &'static str,
|
||||
) -> Result<String, ToolCatalogAnalysisError> {
|
||||
definition
|
||||
.get(field)
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.ok_or(ToolCatalogAnalysisError::DefinitionFieldMissing(field))
|
||||
}
|
||||
|
||||
pub fn tool_definition(name: &str, title: &str, description: &str, input_schema: Value) -> Value {
|
||||
json!({
|
||||
"name": name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_community_mcp::manifest::tool_definitions;
|
||||
use crank_community_mcp::manifest::{analyze_published_tool_catalog, tool_definitions};
|
||||
use crank_core::{
|
||||
ExecutionConfig, HttpMethod, Operation, OperationId, OperationSecurityLevel, OperationStatus,
|
||||
Protocol, RestTarget, Target, ToolDescription, WorkspaceId,
|
||||
@@ -57,6 +57,20 @@ fn marks_destructive_tools_as_two_step_confirmation_calls() {
|
||||
assert_eq!(definition["inputSchema"]["required"], json!(["base"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_budget_uses_the_same_definitions_as_tools_list() {
|
||||
let tool = published_tool();
|
||||
let definitions = tool_definitions(&tool);
|
||||
let analysis = analyze_published_tool_catalog(&[tool]).unwrap();
|
||||
|
||||
assert_eq!(analysis.budget.tool_count, definitions.len());
|
||||
assert_eq!(
|
||||
analysis.budget.serialized_bytes,
|
||||
serde_json::to_vec(&definitions[0]).unwrap().len()
|
||||
);
|
||||
assert!(!analysis.budget.exceeds_recommended_budget);
|
||||
}
|
||||
|
||||
fn published_tool() -> PublishedAgentTool {
|
||||
PublishedAgentTool {
|
||||
workspace_id: WorkspaceId::new("ws_01"),
|
||||
|
||||
Reference in New Issue
Block a user