Skip to main content

dk_agent_sdk/
tools.rs

1//! Programmatic Tool Calling interface for the dkod Agent Protocol.
2//!
3//! Provides tool definitions compatible with Anthropic's `allowed_callers`
4//! mechanism. These definitions can be passed directly to the Messages API
5//! `tools=` parameter, or loaded from the generated `dkod-tools.json` manifest.
6
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9
10const ALLOWED_CALLER: &str = "code_execution_20260120";
11
12/// A single tool definition in Anthropic's tool format.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ToolDefinition {
15    pub name: String,
16    pub description: String,
17    pub input_schema: Value,
18    pub allowed_callers: Vec<String>,
19}
20
21/// Returns all 6 dkod tool definitions for programmatic calling.
22pub fn tool_definitions() -> Vec<ToolDefinition> {
23    vec![
24        ToolDefinition {
25            name: "dkod_connect".into(),
26            description: concat!(
27                "Establish an isolated session workspace on a dkod repository. ",
28                "Returns a session_id, base_commit hash, codebase summary ",
29                "(languages, modules, symbol count), and count of other active ",
30                "sessions. The session workspace is automatically isolated — ",
31                "changes made in this session are invisible to other sessions ",
32                "until merged. Response is JSON: {session_id, base_commit, ",
33                "codebase_summary: {languages, total_symbols, total_files}, ",
34                "active_sessions}."
35            ).into(),
36            input_schema: json!({
37                "type": "object",
38                "properties": {
39                    "codebase": {
40                        "type": "string",
41                        "description": "Repository identifier: 'org/repo'"
42                    },
43                    "intent": {
44                        "type": "string",
45                        "description": "What this agent session intends to accomplish"
46                    },
47                    "mode": {
48                        "type": "string",
49                        "enum": ["ephemeral", "persistent"],
50                        "description": "Ephemeral (default): auto-cleanup on disconnect. Persistent: survives disconnect for later resume."
51                    }
52                },
53                "required": ["codebase", "intent"]
54            }),
55            allowed_callers: vec![ALLOWED_CALLER.into()],
56        },
57        ToolDefinition {
58            name: "dkod_context".into(),
59            description: concat!(
60                "Query semantic context from the codebase. Returns symbols ",
61                "(functions, classes, types) matching the query, with signatures, ",
62                "file locations, call graph edges, and associated tests. ",
63                "Response is JSON: {symbols: [{name, qualified_name, kind, ",
64                "file_path, signature, source, callers, callees}], token_count, ",
65                "freshness}. Results reflect this session's workspace (including ",
66                "uncommitted local changes)."
67            ).into(),
68            input_schema: json!({
69                "type": "object",
70                "properties": {
71                    "session_id": {
72                        "type": "string"
73                    },
74                    "query": {
75                        "type": "string",
76                        "description": "Natural language or structured query: 'All functions that handle user authentication' or 'symbol:authenticate_user'"
77                    },
78                    "depth": {
79                        "type": "string",
80                        "enum": ["signatures", "full", "call_graph"],
81                        "description": "signatures: names + types only. full: complete source. call_graph: signatures + caller/callee edges."
82                    },
83                    "include_tests": {
84                        "type": "boolean"
85                    },
86                    "max_tokens": {
87                        "type": "integer",
88                        "description": "Cap response size in tokens"
89                    }
90                },
91                "required": ["session_id", "query"]
92            }),
93            allowed_callers: vec![ALLOWED_CALLER.into()],
94        },
95        ToolDefinition {
96            name: "dkod_read_file".into(),
97            description: concat!(
98                "Read a file from this session's workspace. Returns the session's ",
99                "view: if the file was modified in this session, returns the ",
100                "modified version; otherwise returns the base version. Response is ",
101                "JSON: {content, hash, modified_in_session}."
102            ).into(),
103            input_schema: json!({
104                "type": "object",
105                "properties": {
106                    "session_id": { "type": "string" },
107                    "path": { "type": "string" }
108                },
109                "required": ["session_id", "path"]
110            }),
111            allowed_callers: vec![ALLOWED_CALLER.into()],
112        },
113        ToolDefinition {
114            name: "dkod_write_file".into(),
115            description: concat!(
116                "Write a file to this session's workspace overlay. The change is ",
117                "only visible to this session until submitted. Response is JSON: ",
118                "{new_hash, detected_changes: [{symbol_name, change_type}]}."
119            ).into(),
120            input_schema: json!({
121                "type": "object",
122                "properties": {
123                    "session_id": { "type": "string" },
124                    "path": { "type": "string" },
125                    "content": { "type": "string" }
126                },
127                "required": ["session_id", "path", "content"]
128            }),
129            allowed_callers: vec![ALLOWED_CALLER.into()],
130        },
131        ToolDefinition {
132            name: "dkod_submit".into(),
133            description: concat!(
134                "Submit this session's changes as a semantic changeset for ",
135                "verification and merge. The platform auto-rebases onto current ",
136                "HEAD if the base moved. Response is JSON with one of: ",
137                "{status: 'accepted', version, changeset_id} or ",
138                "{status: 'verification_failed', failures: [{gate, test_name, ",
139                "error, suggestion}]} or {status: 'conflict', conflicts: [{file, ",
140                "symbol, our_change, their_change}]} or {status: 'pending_review', ",
141                "changeset_id}."
142            ).into(),
143            input_schema: json!({
144                "type": "object",
145                "properties": {
146                    "session_id": { "type": "string" },
147                    "intent": {
148                        "type": "string",
149                        "description": "What this changeset accomplishes"
150                    },
151                    "verify": {
152                        "type": "array",
153                        "items": { "type": "string" },
154                        "description": "Verification gates to run: 'typecheck', 'affected_tests', 'all_tests', 'lint', 'invariants'"
155                    }
156                },
157                "required": ["session_id", "intent"]
158            }),
159            allowed_callers: vec![ALLOWED_CALLER.into()],
160        },
161        ToolDefinition {
162            name: "dkod_session_status".into(),
163            description: concat!(
164                "Get the current state of this session's workspace. Response is ",
165                "JSON: {session_id, base_commit, files_modified, symbols_modified, ",
166                "overlay_size_bytes, active_other_sessions}."
167            ).into(),
168            input_schema: json!({
169                "type": "object",
170                "properties": {
171                    "session_id": { "type": "string" }
172                },
173                "required": ["session_id"]
174            }),
175            allowed_callers: vec![ALLOWED_CALLER.into()],
176        },
177    ]
178}
179
180/// Serialize all tool definitions to a JSON string (for dkod-tools.json).
181pub fn generate_manifest() -> String {
182    serde_json::to_string_pretty(&tool_definitions()).expect("tool definitions are valid JSON")
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_tool_definitions_count() {
191        assert_eq!(tool_definitions().len(), 6);
192    }
193
194    #[test]
195    fn test_all_tools_have_allowed_callers() {
196        for tool in tool_definitions() {
197            assert_eq!(tool.allowed_callers, vec!["code_execution_20260120"]);
198        }
199    }
200
201    #[test]
202    fn test_manifest_is_valid_json() {
203        let manifest = generate_manifest();
204        let parsed: Vec<ToolDefinition> = serde_json::from_str(&manifest).unwrap();
205        assert_eq!(parsed.len(), 6);
206    }
207
208    #[test]
209    fn test_tool_names() {
210        let names: Vec<String> = tool_definitions().iter().map(|t| t.name.clone()).collect();
211        assert!(names.contains(&"dkod_connect".to_string()));
212        assert!(names.contains(&"dkod_context".to_string()));
213        assert!(names.contains(&"dkod_read_file".to_string()));
214        assert!(names.contains(&"dkod_write_file".to_string()));
215        assert!(names.contains(&"dkod_submit".to_string()));
216        assert!(names.contains(&"dkod_session_status".to_string()));
217    }
218
219    #[test]
220    fn test_required_fields_present() {
221        for tool in tool_definitions() {
222            let schema = &tool.input_schema;
223            assert!(schema.get("required").is_some(),
224                "tool {} must have required fields", tool.name);
225            assert!(schema.get("properties").is_some(),
226                "tool {} must have properties", tool.name);
227        }
228    }
229
230    #[test]
231    fn generate_manifest_file() {
232        let manifest = generate_manifest();
233        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
234            .parent().unwrap()  // crates/
235            .parent().unwrap()  // repo root
236            .join("sdk/dkod-tools.json");
237        // Create sdk dir if needed
238        if let Some(parent) = path.parent() {
239            std::fs::create_dir_all(parent).unwrap();
240        }
241        std::fs::write(&path, &manifest).unwrap();
242        println!("Manifest written to {}", path.display());
243    }
244}