Skip to content

Commit 5371e42

Browse files
Make tool description order deterministic (BTreeMap + alphabetical)
ToolRegistry now uses BTreeMap instead of HashMap so generate_description() always iterates tools in alphabetical order. canonical_tool_descriptions() is also sorted alphabetically to match, ensuring GEPA and production produce identical tool prompt strings. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent 0f9b121 commit 5371e42

2 files changed

Lines changed: 42 additions & 41 deletions

File tree

crates/sage-core/src/sage_agent.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
use anyhow::Result;
99
use dspy_rs::{configure, BamlType, ChatAdapter, Predict, LM};
10-
use std::collections::HashMap;
10+
use std::collections::{BTreeMap, HashMap};
1111
use std::sync::Arc;
1212
use uuid::Uuid;
1313

@@ -295,13 +295,13 @@ pub trait Tool: Send + Sync {
295295

296296
/// Registry of available tools
297297
pub struct ToolRegistry {
298-
tools: HashMap<String, Arc<dyn Tool>>,
298+
tools: BTreeMap<String, Arc<dyn Tool>>,
299299
}
300300

301301
impl ToolRegistry {
302302
pub fn new() -> Self {
303303
Self {
304-
tools: HashMap::new(),
304+
tools: BTreeMap::new(),
305305
}
306306
}
307307

crates/sage-core/src/tools.rs

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,66 +7,72 @@ use std::sync::Arc;
77

88
use crate::sage_agent::{Tool, ToolResult};
99

10-
/// Canonical tool descriptions matching exactly what the live Sage agent registers.
11-
/// Used by both the live agent (via ToolRegistry::generate_description) and GEPA evaluation
12-
/// to ensure prompts are identical.
10+
/// Canonical tool descriptions for GEPA evaluation.
1311
///
14-
/// Format matches ToolRegistry::generate_description():
15-
/// {name}:\n Description: {description}\n Args: {args_schema}\n\n
12+
/// Sorted alphabetically by name to match the deterministic BTreeMap order
13+
/// used by ToolRegistry::generate_description() in production. Descriptions
14+
/// are copied verbatim from each Tool impl so both paths produce identical output.
15+
///
16+
/// Note: production may omit tools based on config (e.g. web_search requires
17+
/// BRAVE_API_KEY). GEPA always includes the full set.
1618
#[allow(dead_code)]
1719
pub fn canonical_tool_descriptions() -> String {
18-
// Each entry: (name, description, args_schema)
19-
// Order and content must match what agent_manager.rs registers.
20+
// Alphabetical order to match BTreeMap iteration in ToolRegistry
2021
let tools: &[(&str, &str, &str)] = &[
2122
(
22-
"memory_replace",
23-
"Replace text in a memory block. Requires exact match of old text.",
24-
r#"{"block": "block label (e.g., 'persona', 'human')", "old": "exact text to find", "new": "replacement text"}"#,
23+
"archival_insert",
24+
"Store information in long-term archival memory for future recall. Good for important facts, preferences, and details you want to remember.",
25+
r#"{"content": "text to store", "tags": "optional comma-separated tags"}"#,
2526
),
2627
(
27-
"memory_append",
28-
"Append text to the end of a memory block.",
29-
r#"{"block": "block label (e.g., 'persona', 'human')", "content": "text to append"}"#,
28+
"archival_search",
29+
"Search long-term archival memory using semantic similarity. Returns most relevant stored memories.",
30+
r#"{"query": "search query", "top_k": "max results (default 5)", "tags": "optional comma-separated tags to filter by"}"#,
3031
),
3132
(
32-
"memory_insert",
33-
"Insert text at a specific line in a memory block. Use line=-1 for end.",
34-
r#"{"block": "block label", "content": "text to insert", "line": "line number (0-indexed, -1 for end)"}"#,
33+
"cancel_schedule",
34+
"Cancel a pending scheduled task by ID.",
35+
r#"{"id": "UUID of the task to cancel"}"#,
3536
),
3637
(
3738
"conversation_search",
3839
"Search through past conversation history, including older summarized conversations. Returns matching messages and summaries with relevance scores.",
3940
r#"{"query": "search query", "limit": "max results (default 5)"}"#,
4041
),
4142
(
42-
"archival_insert",
43-
"Store information in long-term archival memory for future recall. Good for important facts, preferences, and details you want to remember.",
44-
r#"{"content": "text to store", "tags": "optional comma-separated tags"}"#,
43+
"done",
44+
"No-op signal. Use ONLY when messages is [] AND no other tools needed. Indicates nothing to do this turn.",
45+
r#"{}"#,
4546
),
4647
(
47-
"archival_search",
48-
"Search long-term archival memory using semantic similarity. Returns most relevant stored memories.",
49-
r#"{"query": "search query", "top_k": "max results (default 5)", "tags": "optional comma-separated tags to filter by"}"#,
48+
"list_schedules",
49+
"List scheduled tasks. By default shows pending tasks only.",
50+
r#"{"status": "optional filter: pending, completed, failed, cancelled, or all (default: pending)"}"#,
5051
),
5152
(
52-
"set_preference",
53-
"Set a user preference. Known keys: 'timezone' (IANA format like 'America/Chicago'), 'language' (ISO code like 'en'), 'display_name'. Other keys are also allowed.",
54-
r#"{"key": "preference key (e.g., 'timezone', 'language', 'display_name')", "value": "preference value"}"#,
53+
"memory_append",
54+
"Append text to the end of a memory block.",
55+
r#"{"block": "block label (e.g., 'persona', 'human')", "content": "text to append"}"#,
56+
),
57+
(
58+
"memory_insert",
59+
"Insert text at a specific line in a memory block. Use line=-1 for end.",
60+
r#"{"block": "block label", "content": "text to insert", "line": "line number (0-indexed, -1 for end)"}"#,
61+
),
62+
(
63+
"memory_replace",
64+
"Replace text in a memory block. Requires exact match of old text.",
65+
r#"{"block": "block label (e.g., 'persona', 'human')", "old": "exact text to find", "new": "replacement text"}"#,
5566
),
5667
(
5768
"schedule_task",
5869
"Schedule a future message or tool execution. Supports one-off (ISO datetime) or recurring (cron expression).",
5970
r#"{"task_type": "message|tool_call", "description": "human-readable description", "run_at": "ISO datetime (2026-01-26T15:30:00Z) or cron (0 9 * * MON-FRI)", "payload": "JSON: {\"message\": \"...\"} for message, {\"tool\": \"name\", \"args\": {...}} for tool_call", "timezone": "optional IANA timezone for cron (default: user preference or UTC)"}"#,
6071
),
6172
(
62-
"list_schedules",
63-
"List scheduled tasks. By default shows pending tasks only.",
64-
r#"{"status": "optional filter: pending, completed, failed, cancelled, or all (default: pending)"}"#,
65-
),
66-
(
67-
"cancel_schedule",
68-
"Cancel a pending scheduled task by ID.",
69-
r#"{"id": "UUID of the task to cancel"}"#,
73+
"set_preference",
74+
"Set a user preference. Known keys: 'timezone' (IANA format like 'America/Chicago'), 'language' (ISO code like 'en'), 'display_name'. Other keys are also allowed.",
75+
r#"{"key": "preference key (e.g., 'timezone', 'language', 'display_name')", "value": "preference value"}"#,
7076
),
7177
(
7278
"shell",
@@ -78,11 +84,6 @@ pub fn canonical_tool_descriptions() -> String {
7884
"Search the web with AI summaries, real-time data (weather, stocks, sports), and rich results. Use 'freshness' for time-sensitive queries, 'location' for local results.",
7985
r#"{ "query": "search query", "count": "results (default 10)", "freshness": "pd=24h, pw=week, pm=month (optional)", "location": "city or 'city, state' for local results (optional)" }"#,
8086
),
81-
(
82-
"done",
83-
"No-op signal. Use ONLY when messages is [] AND no other tools needed. Indicates nothing to do this turn.",
84-
r#"{}"#,
85-
),
8687
];
8788

8889
let mut desc = String::from("Available tools (add to tool_calls array to use):\n\n");

0 commit comments

Comments
 (0)