lean_ctx/tools/registered/
ctx_workflow.rs1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str};
6use crate::tool_defs::tool_def;
7
8pub struct CtxWorkflowTool;
9
10impl McpTool for CtxWorkflowTool {
11 fn name(&self) -> &'static str {
12 "ctx_workflow"
13 }
14
15 fn tool_def(&self) -> Tool {
16 tool_def(
17 "ctx_workflow",
18 "Workflow rails — state machine with evidence tracking.\n\
19 WORKFLOW: start → transition (multiple) → complete. evidence_add before\n\
20 transition to attach proof. Built-in plan_code_test when spec omitted.\n\
21 Actions: start|status|transition|complete|evidence_add|evidence_list|stop.\n\
22 spec=WorkflowSpec JSON for custom states/transitions.\n\
23 ANTIPATTERN: NOT for one-shot tasks — use direct tool calls instead.",
24 json!({
25 "type": "object",
26 "properties": {
27 "action": {
28 "type": "string",
29 "enum": ["start", "status", "transition", "complete", "evidence_add", "evidence_list", "stop"],
30 "description": "start|status|transition|complete|evidence_add|evidence_list|stop"
31 },
32 "name": { "type": "string", "description": "Workflow name (for start)" },
33 "spec": { "type": "string", "description": "WorkflowSpec JSON (for start; omit for builtin)" },
34 "to": { "type": "string", "description": "Target state (for transition)" },
35 "key": { "type": "string", "description": "Evidence key (for evidence_add)" },
36 "value": { "type": "string", "description": "Evidence value or transition note" }
37 },
38 "allOf": [
39 { "if": { "properties": { "action": { "const": "transition" } }, "required": ["action"] },
40 "then": { "required": ["action", "to"] } },
41 { "if": { "properties": { "action": { "const": "evidence_add" } }, "required": ["action"] },
42 "then": { "required": ["action", "key"] } }
43 ]
44 }),
45 )
46 }
47
48 fn handle(
49 &self,
50 args: &Map<String, Value>,
51 ctx: &ToolContext,
52 ) -> Result<ToolOutput, ErrorData> {
53 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
54
55 let agent_id_str = ctx
56 .agent_id
57 .as_ref()
58 .and_then(|h| h.blocking_read().clone());
59
60 let result = {
61 let session_handle = ctx
62 .session
63 .as_ref()
64 .ok_or_else(|| ErrorData::internal_error("session not available", None))?;
65 let mut session = session_handle.blocking_write();
66 crate::tools::ctx_workflow::handle_with_session_agent(
67 Some(args),
68 &mut session,
69 agent_id_str.as_deref(),
70 )
71 };
72
73 if let Some(workflow_handle) = ctx.workflow.as_ref() {
74 let mut wf = workflow_handle.blocking_write();
75 *wf = crate::core::workflow::load_active_for_agent(agent_id_str.as_deref())
76 .ok()
77 .flatten();
78 }
79
80 Ok(ToolOutput {
81 text: result,
82 original_tokens: 0,
83 saved_tokens: 0,
84 mode: Some(action),
85 path: None,
86 changed: false,
87 shell_outcome: None,
88 content_blocks: None,
89 })
90 }
91}