1use super::{Capability, CapabilityStatus, MountPoint, is_declarative_capability};
14use crate::app::{App, AppChannel, ChannelType};
15#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
16use crate::capability_types::{MountAccess, MountSource, VirtualFileTree};
17use crate::tool_types::ToolHints;
18use crate::tools::{Tool, ToolExecutionResult};
19use crate::traits::ToolContext;
20use async_trait::async_trait;
21#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
22use include_dir::{Dir, include_dir};
23use serde_json::{Value, json};
24#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
25use std::sync::Arc;
26
27const SESSION_READ_MESSAGES_DEFAULT_LIMIT: usize = 10;
28const SESSION_READ_MESSAGES_MAX_LIMIT: usize = 50;
29const SESSION_READ_MESSAGES_DEFAULT_CONTENT_LIMIT: usize = 12_000;
30const SESSION_READ_MESSAGES_MAX_CONTENT_LIMIT: usize = 50_000;
31
32#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
42static DOCS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/../../docs");
43
44#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
45fn dir_to_tree(dir: &Dir, base: &str) -> VirtualFileTree {
46 let mut tree = VirtualFileTree::new();
47 tree.insert_directory(base);
48 populate_tree(&mut tree, dir, base);
49 tree
50}
51
52#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
53fn populate_tree(tree: &mut VirtualFileTree, dir: &Dir, prefix: &str) {
54 for file in dir.files() {
55 let name = file
56 .path()
57 .file_name()
58 .and_then(|n| n.to_str())
59 .unwrap_or("");
60 let ext = file
62 .path()
63 .extension()
64 .and_then(|e| e.to_str())
65 .unwrap_or("");
66 if !matches!(ext, "md" | "mdx") {
67 continue;
68 }
69 let path = format!("{prefix}/{name}");
70 let content = std::str::from_utf8(file.contents()).unwrap_or("");
71 tree.insert_text(&path, content);
72 }
73 for subdir in dir.dirs() {
74 let name = subdir
75 .path()
76 .file_name()
77 .and_then(|n| n.to_str())
78 .unwrap_or("");
79 let path = format!("{prefix}/{name}");
80 tree.insert_directory(&path);
81 populate_tree(tree, subdir, &path);
82 }
83}
84
85#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
87fn docs_tree() -> Arc<VirtualFileTree> {
88 use std::sync::OnceLock;
89 static TREE: OnceLock<Arc<VirtualFileTree>> = OnceLock::new();
90 TREE.get_or_init(|| Arc::new(dir_to_tree(&DOCS_DIR, "/docs")))
91 .clone()
92}
93
94#[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
95const SYSTEM_PROMPT: &str = r#"Capabilities extend agent/harness functionality. Three types: built-in, MCP servers, and skills. Use `read_capabilities` to discover IDs before creating agents/harnesses. All results include UI links.
96<platform-docs>
97Platform documentation is available at /workspace/docs in the session filesystem.
98Use `read_file`, `list_directory`, or `grep` to browse and search it.
99Virtual bash commands like `cat /workspace/docs/...`, `ls /workspace/docs/`, and
100`grep -r "pattern" /workspace/docs/` also work.
101
102Key sections:
103- /workspace/docs/getting-started/ — Introduction, concepts, architecture, Docker setup
104- /workspace/docs/features/ — SDK, CLI, UI, events, harnesses, capabilities, apps, skills
105- /workspace/docs/capabilities/ — Per-capability reference (file-system, virtual-bash, web-fetch, etc.)
106- /workspace/docs/integrations/ — External integrations (Slack, Daytona, Browserless, etc.)
107- /workspace/docs/advanced/ — Budgets, compaction, embedding, network access, request signing
108- /workspace/docs/sre/ — Environment variables, admin container, runbooks
109
110When the user asks about Everruns features, configuration, or how things work,
111consult these docs before answering.
112</platform-docs>"#;
113
114#[cfg(not(all(feature = "embedded-platform-docs", everruns_has_workspace_docs)))]
115const SYSTEM_PROMPT: &str = "Capabilities extend agent/harness functionality. Three types: built-in, MCP servers, and skills. Use `read_capabilities` to discover IDs before creating agents/harnesses. All results include UI links.";
116
117pub struct PlatformManagementCapability;
118
119impl Capability for PlatformManagementCapability {
120 fn id(&self) -> &str {
121 "platform_management"
122 }
123
124 fn name(&self) -> &str {
125 "Platform Management"
126 }
127
128 fn description(&self) -> &str {
129 "Tools to manage harnesses, agents, apps, channels, and sessions. Create, list, update, delete entities and interact with sessions programmatically."
130 }
131
132 fn status(&self) -> CapabilityStatus {
133 CapabilityStatus::Available
134 }
135
136 fn icon(&self) -> Option<&str> {
137 Some("settings-2")
138 }
139
140 fn category(&self) -> Option<&str> {
141 Some("Platform")
142 }
143
144 fn system_prompt_addition(&self) -> Option<&str> {
145 Some(SYSTEM_PROMPT)
146 }
147
148 fn tools(&self) -> Vec<Box<dyn Tool>> {
149 vec![
150 Box::new(ReadCapabilitiesTool),
151 Box::new(ReadHarnessesTool),
152 Box::new(ManageHarnessesTool),
153 Box::new(ReadAgentsTool),
154 Box::new(ManageAgentsTool),
155 Box::new(ReadAppsTool),
156 Box::new(ManageAppsTool),
157 Box::new(ManageAppChannelsTool),
158 Box::new(ReadSessionsTool),
159 Box::new(SessionContextReportTool),
160 Box::new(ManageSessionsTool),
161 Box::new(SessionSendMessageTool),
162 Box::new(SessionReadMessagesTool),
163 Box::new(SessionReadResponseTool),
164 ]
165 }
166
167 fn mounts(&self) -> Vec<MountPoint> {
168 #[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
169 {
170 vec![MountPoint::new(
171 "/docs",
172 MountAccess::ReadOnly,
173 MountSource::Virtual { tree: docs_tree() },
174 self.id(),
175 )]
176 }
177 #[cfg(not(all(feature = "embedded-platform-docs", everruns_has_workspace_docs)))]
178 {
179 Vec::new()
180 }
181 }
182
183 fn dependencies(&self) -> Vec<&'static str> {
184 #[cfg(all(feature = "embedded-platform-docs", everruns_has_workspace_docs))]
185 {
186 vec!["session_file_system"]
187 }
188 #[cfg(not(all(feature = "embedded-platform-docs", everruns_has_workspace_docs)))]
189 {
190 Vec::new()
191 }
192 }
193}
194
195fn get_platform_store(
200 context: &ToolContext,
201) -> Result<&dyn crate::platform_store::PlatformStore, ToolExecutionResult> {
202 match &context.platform_store {
203 Some(store) => Ok(store.as_ref()),
204 None => Err(ToolExecutionResult::tool_error(
205 "Platform management not available in this context. Ensure the platform_management capability is enabled.",
206 )),
207 }
208}
209
210fn get_str<'a>(args: &'a Value, key: &str) -> Option<&'a str> {
211 args.get(key)
212 .and_then(|v| v.as_str())
213 .filter(|s| !s.is_empty())
214}
215
216fn require_str<'a>(args: &'a Value, key: &str) -> Result<&'a str, ToolExecutionResult> {
217 get_str(args, key).ok_or_else(|| {
218 ToolExecutionResult::tool_error(format!("Missing required parameter: {key}"))
219 })
220}
221
222fn parse_bounded_usize_arg(
223 args: &Value,
224 key: &str,
225 default: usize,
226 max: usize,
227) -> Result<usize, ToolExecutionResult> {
228 match args.get(key).and_then(|v| v.as_u64()) {
229 Some(0) => Err(ToolExecutionResult::tool_error(format!(
230 "{key} must be greater than 0"
231 ))),
232 Some(value) => Ok((value as usize).min(max)),
233 None => Ok(default),
234 }
235}
236
237fn parse_channel_type(value: &str, field: &str) -> Result<ChannelType, ToolExecutionResult> {
238 serde_json::from_value(Value::String(value.to_string()))
239 .map_err(|_| ToolExecutionResult::tool_error(format!("Invalid {field}: {value}")))
240}
241
242fn truncate_content_chars(content: &str, limit: usize) -> (String, bool, usize, usize) {
243 let mut end_byte = content.len();
244 let mut returned_chars = 0;
245 let mut total_chars = 0;
246
247 for (idx, (byte_idx, _)) in content.char_indices().enumerate() {
248 total_chars = idx + 1;
249 if idx == limit {
250 end_byte = byte_idx;
251 }
252 if idx < limit {
253 returned_chars = idx + 1;
254 }
255 }
256
257 let truncated = total_chars > limit;
258 if !truncated {
259 return (content.to_string(), false, total_chars, total_chars);
260 }
261
262 (
263 content[..end_byte].to_string(),
264 true,
265 total_chars,
266 returned_chars,
267 )
268}
269
270fn channel_json(channel: &AppChannel, include_config: bool) -> Value {
271 let mut json = json!({
272 "id": channel.public_id.to_string(),
273 "channel_type": channel.channel_type.to_string(),
274 "enabled": channel.enabled,
275 "created_at": channel.created_at.to_rfc3339(),
276 "updated_at": channel.updated_at.to_rfc3339(),
277 });
278 if include_config {
279 json["channel_config"] = channel.channel_config.clone();
280 }
281 json
282}
283
284fn app_json(app: &App, base_url: &str, include_channel_config: bool) -> Value {
285 json!({
286 "id": app.public_id.to_string(),
287 "name": app.name,
288 "description": app.description,
289 "status": app.status.to_string(),
290 "harness_id": app.harness_id.to_string(),
291 "agent_id": app.agent_id.as_ref().map(|id| id.to_string()),
292 "agent_identity_id": app.agent_identity_id.as_ref().map(|id| id.to_string()),
293 "published_at": app.published_at.map(|value| value.to_rfc3339()),
294 "created_at": app.created_at.to_rfc3339(),
295 "updated_at": app.updated_at.to_rfc3339(),
296 "channel_count": app.channels.len(),
297 "channels": app
298 .channels
299 .iter()
300 .map(|channel| channel_json(channel, include_channel_config))
301 .collect::<Vec<_>>(),
302 "ui_link": format!("{}/apps/{}", base_url, app.public_id),
303 })
304}
305
306pub struct ReadHarnessesTool;
311
312#[async_trait]
313impl Tool for ReadHarnessesTool {
314 fn name(&self) -> &str {
315 "read_harnesses"
316 }
317
318 fn display_name(&self) -> Option<&str> {
319 Some("Read Harnesses")
320 }
321
322 fn description(&self) -> &str {
323 "Read harnesses by ID or list all. When id is provided returns full detail including system_prompt; otherwise returns summaries."
324 }
325
326 fn parameters_schema(&self) -> Value {
327 json!({
328 "type": "object",
329 "properties": {
330 "id": {
331 "type": "string",
332 "description": "Optional harness ID to get a single harness with full detail (incl. system_prompt)"
333 }
334 },
335 "additionalProperties": false
336 })
337 }
338
339 fn hints(&self) -> ToolHints {
340 ToolHints::default()
341 .with_readonly(true)
342 .with_idempotent(true)
343 }
344
345 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
346 ToolExecutionResult::tool_error(
347 "read_harnesses requires context. This tool must be executed with session context.",
348 )
349 }
350
351 async fn execute_with_context(
352 &self,
353 arguments: Value,
354 context: &ToolContext,
355 ) -> ToolExecutionResult {
356 let store = match get_platform_store(context) {
357 Ok(s) => s,
358 Err(e) => return e,
359 };
360
361 let base_url = store.base_url();
362
363 if let Some(id_str) = get_str(&arguments, "id") {
364 let id = match id_str.parse::<crate::typed_id::HarnessId>() {
365 Ok(id) => id,
366 Err(_) => {
367 return ToolExecutionResult::tool_error(format!(
368 "Invalid harness id: {id_str}"
369 ));
370 }
371 };
372 match store.get_harness(id).await {
373 Ok(Some(h)) => ToolExecutionResult::success(json!({
374 "id": h.id.to_string(),
375 "name": h.name,
376 "display_name": h.display_name,
377 "description": h.description,
378 "system_prompt": h.system_prompt,
379 "status": format!("{:?}", h.status),
380 "capabilities": h.capabilities.iter().map(|c| c.capability_id().to_string()).collect::<Vec<_>>(),
381 "tags": h.tags,
382 "ui_link": format!("{}/harnesses/{}", base_url, h.id),
383 })),
384 Ok(None) => ToolExecutionResult::tool_error(format!("Harness not found: {id_str}")),
385 Err(e) => ToolExecutionResult::tool_error(format!("Failed to get harness: {e}")),
386 }
387 } else {
388 match store.list_harnesses().await {
389 Ok(harnesses) => {
390 let items: Vec<Value> = harnesses
391 .iter()
392 .map(|h| {
393 json!({
394 "id": h.id.to_string(),
395 "name": h.name,
396 "display_name": h.display_name,
397 "description": h.description,
398 "status": format!("{:?}", h.status),
399 "capabilities": h.capabilities.iter().map(|c| c.capability_id().to_string()).collect::<Vec<_>>(),
400 "tags": h.tags,
401 "ui_link": format!("{}/harnesses/{}", base_url, h.id),
402 })
403 })
404 .collect();
405 ToolExecutionResult::success(json!({"harnesses": items, "count": items.len()}))
406 }
407 Err(e) => ToolExecutionResult::tool_error(format!("Failed to list harnesses: {e}")),
408 }
409 }
410 }
411
412 fn requires_context(&self) -> bool {
413 true
414 }
415}
416
417pub struct ManageHarnessesTool;
422
423#[async_trait]
424impl Tool for ManageHarnessesTool {
425 fn name(&self) -> &str {
426 "manage_harnesses"
427 }
428
429 fn display_name(&self) -> Option<&str> {
430 Some("Manage Harnesses")
431 }
432
433 fn description(&self) -> &str {
434 "Harness mutations: create, update, delete, destroy, copy."
435 }
436
437 fn parameters_schema(&self) -> Value {
438 json!({
439 "type": "object",
440 "properties": {
441 "operation": {
442 "type": "string",
443 "enum": ["create", "update", "delete", "copy"],
444 "description": "The mutation to perform"
445 },
446 "harness_id": {
447 "type": "string",
448 "description": "Harness ID (required for update, delete, copy)"
449 },
450 "name": {
451 "type": "string",
452 "description": "Harness name (required for create, optional for update/copy)"
453 },
454 "new_name": {
455 "type": "string",
456 "description": "New name when copying a harness"
457 },
458 "description": {
459 "type": "string",
460 "description": "Harness description"
461 },
462 "system_prompt": {
463 "type": "string",
464 "description": "System prompt for the harness. Defaults to 'You are a helpful assistant.' if omitted."
465 },
466 "parent_harness_id": {
467 "type": ["string", "null"],
468 "description": "Optional parent harness ID. Set to null on update to clear inheritance."
469 },
470 "capabilities": {
471 "type": "array",
472 "items": {"type": "string"},
473 "description": "List of capability IDs"
474 }
475 },
476 "required": ["operation"],
477 "additionalProperties": false
478 })
479 }
480
481 fn hints(&self) -> ToolHints {
482 ToolHints::default().with_narration_noun("harness")
483 }
484
485 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
486 ToolExecutionResult::tool_error(
487 "manage_harnesses requires context. This tool must be executed with session context.",
488 )
489 }
490
491 async fn execute_with_context(
492 &self,
493 arguments: Value,
494 context: &ToolContext,
495 ) -> ToolExecutionResult {
496 let store = match get_platform_store(context) {
497 Ok(s) => s,
498 Err(e) => return e,
499 };
500
501 let operation = match require_str(&arguments, "operation") {
502 Ok(op) => op,
503 Err(e) => return e,
504 };
505
506 let base_url = store.base_url();
507
508 match operation {
509 "create" => {
510 let name = match require_str(&arguments, "name") {
511 Ok(s) => s,
512 Err(e) => return e,
513 };
514 let display_name = match require_str(&arguments, "display_name") {
515 Ok(s) => s,
516 Err(e) => return e,
517 };
518 let system_prompt =
519 get_str(&arguments, "system_prompt").unwrap_or("You are a helpful assistant.");
520 let description = get_str(&arguments, "description");
521 let parent_harness_id = match arguments.get("parent_harness_id") {
522 Some(Value::String(id_str)) => {
523 match id_str.parse::<crate::typed_id::HarnessId>() {
524 Ok(id) => Some(id),
525 Err(_) => {
526 return ToolExecutionResult::tool_error(format!(
527 "Invalid parent_harness_id: {id_str}"
528 ));
529 }
530 }
531 }
532 Some(Value::Null) | None => None,
533 Some(_) => {
534 return ToolExecutionResult::tool_error(
535 "parent_harness_id must be a harness ID string or null",
536 );
537 }
538 };
539 let capabilities: Vec<String> = arguments
540 .get("capabilities")
541 .and_then(|v| v.as_array())
542 .map(|arr| {
543 arr.iter()
544 .filter_map(|v| v.as_str().map(|s| s.to_string()))
545 .collect()
546 })
547 .unwrap_or_default();
548 match store
549 .create_harness(
550 name,
551 Some(display_name),
552 description,
553 system_prompt,
554 parent_harness_id,
555 &capabilities,
556 )
557 .await
558 {
559 Ok(h) => ToolExecutionResult::success(json!({
560 "id": h.id.to_string(),
561 "name": h.name,
562 "display_name": h.display_name,
563 "description": h.description,
564 "parent_harness_id": h.parent_harness_id.map(|id| id.to_string()),
565 "status": format!("{:?}", h.status),
566 "ui_link": format!("{}/harnesses/{}", base_url, h.id),
567 "message": "Harness created successfully"
568 })),
569 Err(e) => {
570 ToolExecutionResult::tool_error(format!("Failed to create harness: {e}"))
571 }
572 }
573 }
574
575 "update" => {
576 let id_str = match require_str(&arguments, "harness_id") {
577 Ok(s) => s,
578 Err(e) => return e,
579 };
580 let id = match id_str.parse::<crate::typed_id::HarnessId>() {
581 Ok(id) => id,
582 Err(_) => {
583 return ToolExecutionResult::tool_error(format!(
584 "Invalid harness_id: {id_str}"
585 ));
586 }
587 };
588 let name = get_str(&arguments, "name");
589 let display_name = get_str(&arguments, "display_name");
590 let description = get_str(&arguments, "description");
591 let system_prompt = get_str(&arguments, "system_prompt");
592 let parent_harness_id = match arguments.get("parent_harness_id") {
593 Some(Value::String(id_str)) => {
594 match id_str.parse::<crate::typed_id::HarnessId>() {
595 Ok(id) => Some(Some(id)),
596 Err(_) => {
597 return ToolExecutionResult::tool_error(format!(
598 "Invalid parent_harness_id: {id_str}"
599 ));
600 }
601 }
602 }
603 Some(Value::Null) => Some(None),
604 None => None,
605 Some(_) => {
606 return ToolExecutionResult::tool_error(
607 "parent_harness_id must be a harness ID string or null",
608 );
609 }
610 };
611 match store
612 .update_harness(
613 id,
614 name,
615 display_name,
616 description,
617 system_prompt,
618 parent_harness_id,
619 )
620 .await
621 {
622 Ok(h) => ToolExecutionResult::success(json!({
623 "id": h.id.to_string(),
624 "name": h.name,
625 "display_name": h.display_name,
626 "description": h.description,
627 "parent_harness_id": h.parent_harness_id.map(|id| id.to_string()),
628 "status": format!("{:?}", h.status),
629 "ui_link": format!("{}/harnesses/{}", base_url, h.id),
630 "message": "Harness updated successfully"
631 })),
632 Err(e) => {
633 ToolExecutionResult::tool_error(format!("Failed to update harness: {e}"))
634 }
635 }
636 }
637
638 "delete" => {
639 let id_str = match require_str(&arguments, "harness_id") {
640 Ok(s) => s,
641 Err(e) => return e,
642 };
643 let id = match id_str.parse::<crate::typed_id::HarnessId>() {
644 Ok(id) => id,
645 Err(_) => {
646 return ToolExecutionResult::tool_error(format!(
647 "Invalid harness_id: {id_str}"
648 ));
649 }
650 };
651 match store.delete_harness(id).await {
652 Ok(()) => ToolExecutionResult::success(json!({
653 "harness_id": id_str,
654 "ui_link": format!("{}/harnesses/{}", base_url, id_str),
655 "message": "Harness archived successfully"
656 })),
657 Err(e) => {
658 ToolExecutionResult::tool_error(format!("Failed to delete harness: {e}"))
659 }
660 }
661 }
662
663 "copy" => {
664 let id_str = match require_str(&arguments, "harness_id") {
665 Ok(s) => s,
666 Err(e) => return e,
667 };
668 let id = match id_str.parse::<crate::typed_id::HarnessId>() {
669 Ok(id) => id,
670 Err(_) => {
671 return ToolExecutionResult::tool_error(format!(
672 "Invalid harness_id: {id_str}"
673 ));
674 }
675 };
676 let new_name = get_str(&arguments, "new_name");
677 match store.copy_harness(id, new_name).await {
678 Ok(h) => ToolExecutionResult::success(json!({
679 "id": h.id.to_string(),
680 "name": h.name,
681 "display_name": h.display_name,
682 "description": h.description,
683 "status": format!("{:?}", h.status),
684 "ui_link": format!("{}/harnesses/{}", base_url, h.id),
685 "source_harness_id": id_str,
686 "message": "Harness copied successfully"
687 })),
688 Err(e) => {
689 ToolExecutionResult::tool_error(format!("Failed to copy harness: {e}"))
690 }
691 }
692 }
693
694 _ => ToolExecutionResult::tool_error(format!(
695 "Unknown operation: {operation}. Valid: create, update, delete, copy"
696 )),
697 }
698 }
699
700 fn requires_context(&self) -> bool {
701 true
702 }
703}
704
705pub struct ReadAgentsTool;
710
711#[async_trait]
712impl Tool for ReadAgentsTool {
713 fn name(&self) -> &str {
714 "read_agents"
715 }
716
717 fn display_name(&self) -> Option<&str> {
718 Some("Read Agents")
719 }
720
721 fn description(&self) -> &str {
722 "Read agents by ID or list all. When id is provided returns full detail including system_prompt; otherwise returns summaries."
723 }
724
725 fn parameters_schema(&self) -> Value {
726 json!({
727 "type": "object",
728 "properties": {
729 "id": {
730 "type": "string",
731 "description": "Optional agent ID to get a single agent with full detail (incl. system_prompt)"
732 }
733 },
734 "additionalProperties": false
735 })
736 }
737
738 fn hints(&self) -> ToolHints {
739 ToolHints::default()
740 .with_readonly(true)
741 .with_idempotent(true)
742 }
743
744 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
745 ToolExecutionResult::tool_error(
746 "read_agents requires context. This tool must be executed with session context.",
747 )
748 }
749
750 async fn execute_with_context(
751 &self,
752 arguments: Value,
753 context: &ToolContext,
754 ) -> ToolExecutionResult {
755 let store = match get_platform_store(context) {
756 Ok(s) => s,
757 Err(e) => return e,
758 };
759
760 let base_url = store.base_url();
761
762 if let Some(id_str) = get_str(&arguments, "id") {
763 let id = match id_str.parse::<crate::typed_id::AgentId>() {
764 Ok(id) => id,
765 Err(_) => {
766 return ToolExecutionResult::tool_error(format!("Invalid agent id: {id_str}"));
767 }
768 };
769 match store.get_agent_by_id(id).await {
770 Ok(Some(a)) => ToolExecutionResult::success(json!({
771 "id": a.public_id.to_string(),
772 "name": a.name,
773 "display_name": a.display_name,
774 "description": a.description,
775 "system_prompt": a.system_prompt,
776 "status": format!("{:?}", a.status),
777 "capabilities": a.capabilities.iter().map(|c| c.capability_id().to_string()).collect::<Vec<_>>(),
778 "tags": a.tags,
779 "ui_link": format!("{}/agents/{}", base_url, a.public_id),
780 })),
781 Ok(None) => ToolExecutionResult::tool_error(format!("Agent not found: {id_str}")),
782 Err(e) => ToolExecutionResult::tool_error(format!("Failed to get agent: {e}")),
783 }
784 } else {
785 match store.list_agents().await {
786 Ok(agents) => {
787 let items: Vec<Value> = agents
788 .iter()
789 .map(|a| {
790 json!({
791 "id": a.public_id.to_string(),
792 "name": a.name,
793 "display_name": a.display_name,
794 "description": a.description,
795 "status": format!("{:?}", a.status),
796 "capabilities": a.capabilities.iter().map(|c| c.capability_id().to_string()).collect::<Vec<_>>(),
797 "tags": a.tags,
798 "ui_link": format!("{}/agents/{}", base_url, a.public_id),
799 })
800 })
801 .collect();
802 ToolExecutionResult::success(json!({"agents": items, "count": items.len()}))
803 }
804 Err(e) => ToolExecutionResult::tool_error(format!("Failed to list agents: {e}")),
805 }
806 }
807 }
808
809 fn requires_context(&self) -> bool {
810 true
811 }
812}
813
814pub struct ManageAgentsTool;
819
820#[async_trait]
821impl Tool for ManageAgentsTool {
822 fn name(&self) -> &str {
823 "manage_agents"
824 }
825
826 fn display_name(&self) -> Option<&str> {
827 Some("Manage Agents")
828 }
829
830 fn description(&self) -> &str {
831 "Agent mutations: create, update, delete, destroy."
832 }
833
834 fn parameters_schema(&self) -> Value {
835 json!({
836 "type": "object",
837 "properties": {
838 "operation": {
839 "type": "string",
840 "enum": ["create", "update", "delete"],
841 "description": "The mutation to perform"
842 },
843 "agent_id": {
844 "type": "string",
845 "description": "Agent ID (required for update, delete)"
846 },
847 "name": {
848 "type": "string",
849 "description": "Addressable agent name (required for create). Lowercase letters, numbers, and hyphens only (e.g. 'customer-support')."
850 },
851 "display_name": {
852 "type": "string",
853 "description": "Human-readable display name shown in UI (e.g. 'Customer Support Agent'). Falls back to name when absent."
854 },
855 "description": {
856 "type": "string",
857 "description": "Agent description"
858 },
859 "system_prompt": {
860 "type": "string",
861 "description": "System prompt for the agent. Defaults to 'You are a helpful assistant.' if omitted."
862 },
863 "capabilities": {
864 "type": "array",
865 "items": {"type": "string"},
866 "description": "List of capability IDs"
867 }
868 },
869 "required": ["operation"],
870 "additionalProperties": false
871 })
872 }
873
874 fn hints(&self) -> ToolHints {
875 ToolHints::default().with_narration_noun("agent")
876 }
877
878 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
879 ToolExecutionResult::tool_error(
880 "manage_agents requires context. This tool must be executed with session context.",
881 )
882 }
883
884 async fn execute_with_context(
885 &self,
886 arguments: Value,
887 context: &ToolContext,
888 ) -> ToolExecutionResult {
889 let store = match get_platform_store(context) {
890 Ok(s) => s,
891 Err(e) => return e,
892 };
893
894 let operation = match require_str(&arguments, "operation") {
895 Ok(op) => op,
896 Err(e) => return e,
897 };
898
899 let base_url = store.base_url();
900
901 match operation {
902 "create" => {
903 let name = match require_str(&arguments, "name") {
904 Ok(s) => s,
905 Err(e) => return e,
906 };
907 if let Err(msg) = crate::agent::validate_addressable_name(name) {
908 return ToolExecutionResult::tool_error(format!("Invalid agent name: {msg}"));
909 }
910 let display_name = get_str(&arguments, "display_name");
911 let system_prompt =
912 get_str(&arguments, "system_prompt").unwrap_or("You are a helpful assistant.");
913 let description = get_str(&arguments, "description");
914 let capabilities: Vec<String> = arguments
915 .get("capabilities")
916 .and_then(|v| v.as_array())
917 .map(|arr| {
918 arr.iter()
919 .filter_map(|v| v.as_str().map(|s| s.to_string()))
920 .collect()
921 })
922 .unwrap_or_default();
923 match store
924 .create_agent(
925 name,
926 display_name,
927 description,
928 system_prompt,
929 &capabilities,
930 )
931 .await
932 {
933 Ok(a) => ToolExecutionResult::success(json!({
934 "id": a.public_id.to_string(),
935 "name": a.name,
936 "display_name": a.display_name,
937 "description": a.description,
938 "status": format!("{:?}", a.status),
939 "ui_link": format!("{}/agents/{}", base_url, a.public_id),
940 "message": "Agent created successfully"
941 })),
942 Err(e) => {
943 ToolExecutionResult::tool_error(format!("Failed to create agent: {e}"))
944 }
945 }
946 }
947
948 "update" => {
949 let id_str = match require_str(&arguments, "agent_id") {
950 Ok(s) => s,
951 Err(e) => return e,
952 };
953 let id = match id_str.parse::<crate::typed_id::AgentId>() {
954 Ok(id) => id,
955 Err(_) => {
956 return ToolExecutionResult::tool_error(format!(
957 "Invalid agent_id: {id_str}"
958 ));
959 }
960 };
961 let name = get_str(&arguments, "name");
962 if let Some(n) = name
963 && let Err(msg) = crate::agent::validate_addressable_name(n)
964 {
965 return ToolExecutionResult::tool_error(format!("Invalid agent name: {msg}"));
966 }
967 let display_name = get_str(&arguments, "display_name");
968 let description = get_str(&arguments, "description");
969 let system_prompt = get_str(&arguments, "system_prompt");
970 match store
971 .update_agent(id, name, display_name, description, system_prompt)
972 .await
973 {
974 Ok(a) => ToolExecutionResult::success(json!({
975 "id": a.public_id.to_string(),
976 "name": a.name,
977 "display_name": a.display_name,
978 "description": a.description,
979 "status": format!("{:?}", a.status),
980 "ui_link": format!("{}/agents/{}", base_url, a.public_id),
981 "message": "Agent updated successfully"
982 })),
983 Err(e) => {
984 ToolExecutionResult::tool_error(format!("Failed to update agent: {e}"))
985 }
986 }
987 }
988
989 "delete" => {
990 let id_str = match require_str(&arguments, "agent_id") {
991 Ok(s) => s,
992 Err(e) => return e,
993 };
994 let id = match id_str.parse::<crate::typed_id::AgentId>() {
995 Ok(id) => id,
996 Err(_) => {
997 return ToolExecutionResult::tool_error(format!(
998 "Invalid agent_id: {id_str}"
999 ));
1000 }
1001 };
1002 match store.delete_agent(id).await {
1003 Ok(()) => ToolExecutionResult::success(json!({
1004 "agent_id": id_str,
1005 "ui_link": format!("{}/agents/{}", base_url, id_str),
1006 "message": "Agent archived successfully"
1007 })),
1008 Err(e) => {
1009 ToolExecutionResult::tool_error(format!("Failed to delete agent: {e}"))
1010 }
1011 }
1012 }
1013
1014 _ => ToolExecutionResult::tool_error(format!(
1015 "Unknown operation: {operation}. Valid: create, update, delete"
1016 )),
1017 }
1018 }
1019
1020 fn requires_context(&self) -> bool {
1021 true
1022 }
1023}
1024
1025pub struct ReadAppsTool;
1030
1031#[async_trait]
1032impl Tool for ReadAppsTool {
1033 fn name(&self) -> &str {
1034 "read_apps"
1035 }
1036
1037 fn display_name(&self) -> Option<&str> {
1038 Some("Read Apps")
1039 }
1040
1041 fn description(&self) -> &str {
1042 "Read apps by ID or list/filter. When id is provided returns full app detail including channels."
1043 }
1044
1045 fn parameters_schema(&self) -> Value {
1046 json!({
1047 "type": "object",
1048 "properties": {
1049 "id": {
1050 "type": "string",
1051 "description": "Optional app ID to get a single app with channel details"
1052 },
1053 "search": {
1054 "type": "string",
1055 "description": "Optional case-insensitive search by app name or description"
1056 },
1057 "include_archived": {
1058 "type": "boolean",
1059 "description": "Include archived apps in list results (default: false)"
1060 }
1061 },
1062 "additionalProperties": false
1063 })
1064 }
1065
1066 fn hints(&self) -> ToolHints {
1067 ToolHints::default()
1068 .with_readonly(true)
1069 .with_idempotent(true)
1070 }
1071
1072 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1073 ToolExecutionResult::tool_error(
1074 "read_apps requires context. This tool must be executed with session context.",
1075 )
1076 }
1077
1078 async fn execute_with_context(
1079 &self,
1080 arguments: Value,
1081 context: &ToolContext,
1082 ) -> ToolExecutionResult {
1083 let store = match get_platform_store(context) {
1084 Ok(s) => s,
1085 Err(e) => return e,
1086 };
1087
1088 let base_url = store.base_url();
1089
1090 if let Some(id_str) = get_str(&arguments, "id") {
1091 let id = match id_str.parse::<crate::typed_id::AppId>() {
1092 Ok(id) => id,
1093 Err(_) => {
1094 return ToolExecutionResult::tool_error(format!("Invalid app id: {id_str}"));
1095 }
1096 };
1097 match store.get_app(id).await {
1098 Ok(Some(app)) => ToolExecutionResult::success(app_json(&app, base_url, true)),
1099 Ok(None) => ToolExecutionResult::tool_error(format!("App not found: {id_str}")),
1100 Err(e) => ToolExecutionResult::tool_error(format!("Failed to get app: {e}")),
1101 }
1102 } else {
1103 let search = get_str(&arguments, "search");
1104 let include_archived = arguments
1105 .get("include_archived")
1106 .and_then(|value| value.as_bool())
1107 .unwrap_or(false);
1108 match store.list_apps(search, include_archived).await {
1109 Ok(apps) => {
1110 let items = apps
1111 .iter()
1112 .map(|app| app_json(app, base_url, false))
1113 .collect::<Vec<_>>();
1114 ToolExecutionResult::success(json!({"apps": items, "count": items.len()}))
1115 }
1116 Err(e) => ToolExecutionResult::tool_error(format!("Failed to list apps: {e}")),
1117 }
1118 }
1119 }
1120
1121 fn requires_context(&self) -> bool {
1122 true
1123 }
1124}
1125
1126pub struct ManageAppsTool;
1131
1132#[async_trait]
1133impl Tool for ManageAppsTool {
1134 fn name(&self) -> &str {
1135 "manage_apps"
1136 }
1137
1138 fn display_name(&self) -> Option<&str> {
1139 Some("Manage Apps")
1140 }
1141
1142 fn description(&self) -> &str {
1143 "App mutations: create, update, delete (archive), destroy, publish, unpublish."
1144 }
1145
1146 fn parameters_schema(&self) -> Value {
1147 json!({
1148 "type": "object",
1149 "properties": {
1150 "operation": {
1151 "type": "string",
1152 "enum": ["create", "update", "delete", "destroy", "publish", "unpublish"],
1153 "description": "The mutation to perform"
1154 },
1155 "app_id": {
1156 "type": "string",
1157 "description": "App ID (required for update/delete/destroy/publish/unpublish)"
1158 },
1159 "name": {
1160 "type": "string",
1161 "description": "App name (required for create)"
1162 },
1163 "description": {
1164 "type": "string",
1165 "description": "App description (optional)"
1166 },
1167 "harness_id": {
1168 "type": "string",
1169 "description": "Harness ID (required for create)"
1170 },
1171 "agent_id": {
1172 "type": "string",
1173 "description": "Optional agent ID"
1174 },
1175 "agent_identity_id": {
1176 "type": ["string", "null"],
1177 "description": "Optional agent identity ID. Pass null on update to clear it."
1178 },
1179 "channel_type": {
1180 "type": "string",
1181 "enum": ["slack", "ag_ui", "schedule", "webhook", "a2a", "fcp"],
1182 "description": "Optional initial channel type for create"
1183 },
1184 "channel_config": {
1185 "type": "object",
1186 "description": "Optional initial channel configuration for create"
1187 }
1188 },
1189 "required": ["operation"],
1190 "additionalProperties": false
1191 })
1192 }
1193
1194 fn hints(&self) -> ToolHints {
1195 ToolHints::default().with_narration_noun("app")
1196 }
1197
1198 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1199 ToolExecutionResult::tool_error(
1200 "manage_apps requires context. This tool must be executed with session context.",
1201 )
1202 }
1203
1204 async fn execute_with_context(
1205 &self,
1206 arguments: Value,
1207 context: &ToolContext,
1208 ) -> ToolExecutionResult {
1209 let store = match get_platform_store(context) {
1210 Ok(s) => s,
1211 Err(e) => return e,
1212 };
1213
1214 let operation = match require_str(&arguments, "operation") {
1215 Ok(op) => op,
1216 Err(e) => return e,
1217 };
1218
1219 let base_url = store.base_url();
1220
1221 match operation {
1222 "create" => {
1223 let name = match require_str(&arguments, "name") {
1224 Ok(s) => s,
1225 Err(e) => return e,
1226 };
1227 let harness_id_str = match require_str(&arguments, "harness_id") {
1228 Ok(s) => s,
1229 Err(e) => return e,
1230 };
1231 let harness_id = match harness_id_str.parse::<crate::typed_id::HarnessId>() {
1232 Ok(id) => id,
1233 Err(_) => {
1234 return ToolExecutionResult::tool_error(format!(
1235 "Invalid harness_id: {harness_id_str}"
1236 ));
1237 }
1238 };
1239 let description = get_str(&arguments, "description");
1240 let agent_id = match get_str(&arguments, "agent_id") {
1241 Some(value) => match value.parse::<crate::typed_id::AgentId>() {
1242 Ok(id) => Some(id),
1243 Err(_) => {
1244 return ToolExecutionResult::tool_error(format!(
1245 "Invalid agent_id: {value}"
1246 ));
1247 }
1248 },
1249 None => None,
1250 };
1251 let agent_identity_id = if let Some(value) = arguments.get("agent_identity_id") {
1252 if value.is_null() {
1253 None
1254 } else if let Some(value) = value.as_str() {
1255 match value.parse::<crate::typed_id::AgentIdentityId>() {
1256 Ok(id) => Some(id),
1257 Err(_) => {
1258 return ToolExecutionResult::tool_error(format!(
1259 "Invalid agent_identity_id: {value}"
1260 ));
1261 }
1262 }
1263 } else {
1264 return ToolExecutionResult::tool_error(
1265 "agent_identity_id must be a string or null",
1266 );
1267 }
1268 } else {
1269 None
1270 };
1271 let channel_type = match get_str(&arguments, "channel_type") {
1272 Some(value) => match parse_channel_type(value, "channel_type") {
1273 Ok(channel_type) => Some(channel_type),
1274 Err(error) => return error,
1275 },
1276 None => None,
1277 };
1278 let channel_config = arguments.get("channel_config");
1279
1280 match store
1281 .create_app(
1282 name,
1283 description,
1284 harness_id,
1285 agent_id,
1286 agent_identity_id,
1287 channel_type,
1288 channel_config,
1289 )
1290 .await
1291 {
1292 Ok(app) => {
1293 let mut response = app_json(&app, base_url, true);
1294 response["message"] = Value::String("App created successfully".to_string());
1295 ToolExecutionResult::success(response)
1296 }
1297 Err(e) => ToolExecutionResult::tool_error(format!("Failed to create app: {e}")),
1298 }
1299 }
1300
1301 "update" => {
1302 let app_id_str = match require_str(&arguments, "app_id") {
1303 Ok(s) => s,
1304 Err(e) => return e,
1305 };
1306 let app_id = match app_id_str.parse::<crate::typed_id::AppId>() {
1307 Ok(id) => id,
1308 Err(_) => {
1309 return ToolExecutionResult::tool_error(format!(
1310 "Invalid app_id: {app_id_str}"
1311 ));
1312 }
1313 };
1314 let harness_id = match get_str(&arguments, "harness_id") {
1315 Some(value) => match value.parse::<crate::typed_id::HarnessId>() {
1316 Ok(id) => Some(id),
1317 Err(_) => {
1318 return ToolExecutionResult::tool_error(format!(
1319 "Invalid harness_id: {value}"
1320 ));
1321 }
1322 },
1323 None => None,
1324 };
1325 let agent_id = match get_str(&arguments, "agent_id") {
1326 Some(value) => match value.parse::<crate::typed_id::AgentId>() {
1327 Ok(id) => Some(id),
1328 Err(_) => {
1329 return ToolExecutionResult::tool_error(format!(
1330 "Invalid agent_id: {value}"
1331 ));
1332 }
1333 },
1334 None => None,
1335 };
1336 let agent_identity_id = if let Some(value) = arguments.get("agent_identity_id") {
1337 if value.is_null() {
1338 Some(None)
1339 } else if let Some(value) = value.as_str() {
1340 match value.parse::<crate::typed_id::AgentIdentityId>() {
1341 Ok(id) => Some(Some(id)),
1342 Err(_) => {
1343 return ToolExecutionResult::tool_error(format!(
1344 "Invalid agent_identity_id: {value}"
1345 ));
1346 }
1347 }
1348 } else {
1349 return ToolExecutionResult::tool_error(
1350 "agent_identity_id must be a string or null",
1351 );
1352 }
1353 } else {
1354 None
1355 };
1356
1357 match store
1358 .update_app(
1359 app_id,
1360 get_str(&arguments, "name"),
1361 get_str(&arguments, "description"),
1362 harness_id,
1363 agent_id,
1364 agent_identity_id,
1365 )
1366 .await
1367 {
1368 Ok(app) => {
1369 let mut response = app_json(&app, base_url, true);
1370 response["message"] = Value::String("App updated successfully".to_string());
1371 ToolExecutionResult::success(response)
1372 }
1373 Err(e) => ToolExecutionResult::tool_error(format!("Failed to update app: {e}")),
1374 }
1375 }
1376
1377 "delete" => {
1378 let app_id_str = match require_str(&arguments, "app_id") {
1379 Ok(s) => s,
1380 Err(e) => return e,
1381 };
1382 let app_id = match app_id_str.parse::<crate::typed_id::AppId>() {
1383 Ok(id) => id,
1384 Err(_) => {
1385 return ToolExecutionResult::tool_error(format!(
1386 "Invalid app_id: {app_id_str}"
1387 ));
1388 }
1389 };
1390 match store.delete_app(app_id).await {
1391 Ok(()) => ToolExecutionResult::success(json!({
1392 "app_id": app_id_str,
1393 "ui_link": format!("{}/apps/{}", base_url, app_id_str),
1394 "message": "App archived successfully"
1395 })),
1396 Err(e) => {
1397 ToolExecutionResult::tool_error(format!("Failed to archive app: {e}"))
1398 }
1399 }
1400 }
1401
1402 "destroy" => {
1403 let app_id_str = match require_str(&arguments, "app_id") {
1404 Ok(s) => s,
1405 Err(e) => return e,
1406 };
1407 let app_id = match app_id_str.parse::<crate::typed_id::AppId>() {
1408 Ok(id) => id,
1409 Err(_) => {
1410 return ToolExecutionResult::tool_error(format!(
1411 "Invalid app_id: {app_id_str}"
1412 ));
1413 }
1414 };
1415 match store.destroy_app(app_id).await {
1416 Ok(()) => ToolExecutionResult::success(json!({
1417 "app_id": app_id_str,
1418 "ui_link": format!("{}/apps", base_url),
1419 "message": "App destroyed successfully"
1420 })),
1421 Err(e) => {
1422 ToolExecutionResult::tool_error(format!("Failed to destroy app: {e}"))
1423 }
1424 }
1425 }
1426
1427 "publish" => {
1428 let app_id_str = match require_str(&arguments, "app_id") {
1429 Ok(s) => s,
1430 Err(e) => return e,
1431 };
1432 let app_id = match app_id_str.parse::<crate::typed_id::AppId>() {
1433 Ok(id) => id,
1434 Err(_) => {
1435 return ToolExecutionResult::tool_error(format!(
1436 "Invalid app_id: {app_id_str}"
1437 ));
1438 }
1439 };
1440 match store.publish_app(app_id).await {
1441 Ok(app) => {
1442 let mut response = app_json(&app, base_url, true);
1443 response["message"] =
1444 Value::String("App published successfully".to_string());
1445 ToolExecutionResult::success(response)
1446 }
1447 Err(e) => {
1448 ToolExecutionResult::tool_error(format!("Failed to publish app: {e}"))
1449 }
1450 }
1451 }
1452
1453 "unpublish" => {
1454 let app_id_str = match require_str(&arguments, "app_id") {
1455 Ok(s) => s,
1456 Err(e) => return e,
1457 };
1458 let app_id = match app_id_str.parse::<crate::typed_id::AppId>() {
1459 Ok(id) => id,
1460 Err(_) => {
1461 return ToolExecutionResult::tool_error(format!(
1462 "Invalid app_id: {app_id_str}"
1463 ));
1464 }
1465 };
1466 match store.unpublish_app(app_id).await {
1467 Ok(app) => {
1468 let mut response = app_json(&app, base_url, true);
1469 response["message"] =
1470 Value::String("App unpublished successfully".to_string());
1471 ToolExecutionResult::success(response)
1472 }
1473 Err(e) => {
1474 ToolExecutionResult::tool_error(format!("Failed to unpublish app: {e}"))
1475 }
1476 }
1477 }
1478
1479 _ => ToolExecutionResult::tool_error(format!(
1480 "Unknown operation: {operation}. Valid: create, update, delete, destroy, publish, unpublish"
1481 )),
1482 }
1483 }
1484
1485 fn requires_context(&self) -> bool {
1486 true
1487 }
1488}
1489
1490pub struct ManageAppChannelsTool;
1495
1496#[async_trait]
1497impl Tool for ManageAppChannelsTool {
1498 fn name(&self) -> &str {
1499 "manage_app_channels"
1500 }
1501
1502 fn display_name(&self) -> Option<&str> {
1503 Some("Manage App Channels")
1504 }
1505
1506 fn description(&self) -> &str {
1507 "App channel mutations: add, update, delete."
1508 }
1509
1510 fn parameters_schema(&self) -> Value {
1511 json!({
1512 "type": "object",
1513 "properties": {
1514 "operation": {
1515 "type": "string",
1516 "enum": ["add", "update", "delete"],
1517 "description": "The channel mutation to perform"
1518 },
1519 "app_id": {
1520 "type": "string",
1521 "description": "App ID"
1522 },
1523 "channel_id": {
1524 "type": "string",
1525 "description": "Channel ID (required for update/delete)"
1526 },
1527 "channel_type": {
1528 "type": "string",
1529 "enum": ["slack", "ag_ui", "schedule", "webhook", "a2a", "fcp"],
1530 "description": "Channel type (required for add, optional for update)"
1531 },
1532 "channel_config": {
1533 "type": "object",
1534 "description": "Channel-specific configuration object"
1535 },
1536 "enabled": {
1537 "type": "boolean",
1538 "description": "Whether the channel is enabled"
1539 }
1540 },
1541 "required": ["operation", "app_id"],
1542 "additionalProperties": false
1543 })
1544 }
1545
1546 fn hints(&self) -> ToolHints {
1547 ToolHints::default().with_narration_noun("app channel")
1548 }
1549
1550 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1551 ToolExecutionResult::tool_error(
1552 "manage_app_channels requires context. This tool must be executed with session context.",
1553 )
1554 }
1555
1556 async fn execute_with_context(
1557 &self,
1558 arguments: Value,
1559 context: &ToolContext,
1560 ) -> ToolExecutionResult {
1561 let store = match get_platform_store(context) {
1562 Ok(s) => s,
1563 Err(e) => return e,
1564 };
1565
1566 let operation = match require_str(&arguments, "operation") {
1567 Ok(op) => op,
1568 Err(e) => return e,
1569 };
1570
1571 let app_id_str = match require_str(&arguments, "app_id") {
1572 Ok(s) => s,
1573 Err(e) => return e,
1574 };
1575 let app_id = match app_id_str.parse::<crate::typed_id::AppId>() {
1576 Ok(id) => id,
1577 Err(_) => {
1578 return ToolExecutionResult::tool_error(format!("Invalid app_id: {app_id_str}"));
1579 }
1580 };
1581 let base_url = store.base_url();
1582
1583 match operation {
1584 "add" => {
1585 let channel_type_str = match require_str(&arguments, "channel_type") {
1586 Ok(s) => s,
1587 Err(e) => return e,
1588 };
1589 let channel_type = match parse_channel_type(channel_type_str, "channel_type") {
1590 Ok(channel_type) => channel_type,
1591 Err(error) => return error,
1592 };
1593 let channel_config = arguments.get("channel_config");
1594 let enabled = arguments.get("enabled").and_then(|value| value.as_bool());
1595 match store
1596 .add_app_channel(app_id, channel_type, channel_config, enabled)
1597 .await
1598 {
1599 Ok(channel) => ToolExecutionResult::success(json!({
1600 "app_id": app_id_str,
1601 "channel": channel_json(&channel, true),
1602 "ui_link": format!("{}/apps/{}", base_url, app_id),
1603 "message": "App channel added successfully"
1604 })),
1605 Err(e) => {
1606 ToolExecutionResult::tool_error(format!("Failed to add app channel: {e}"))
1607 }
1608 }
1609 }
1610
1611 "update" => {
1612 let channel_id_str = match require_str(&arguments, "channel_id") {
1613 Ok(s) => s,
1614 Err(e) => return e,
1615 };
1616 let channel_id = match channel_id_str.parse::<crate::typed_id::AppChannelId>() {
1617 Ok(id) => id,
1618 Err(_) => {
1619 return ToolExecutionResult::tool_error(format!(
1620 "Invalid channel_id: {channel_id_str}"
1621 ));
1622 }
1623 };
1624 let channel_type = match get_str(&arguments, "channel_type") {
1625 Some(value) => match parse_channel_type(value, "channel_type") {
1626 Ok(channel_type) => Some(channel_type),
1627 Err(error) => return error,
1628 },
1629 None => None,
1630 };
1631 let channel_config = arguments.get("channel_config");
1632 let enabled = arguments.get("enabled").and_then(|value| value.as_bool());
1633 match store
1634 .update_app_channel(app_id, channel_id, channel_type, channel_config, enabled)
1635 .await
1636 {
1637 Ok(channel) => ToolExecutionResult::success(json!({
1638 "app_id": app_id_str,
1639 "channel": channel_json(&channel, true),
1640 "ui_link": format!("{}/apps/{}", base_url, app_id),
1641 "message": "App channel updated successfully"
1642 })),
1643 Err(e) => ToolExecutionResult::tool_error(format!(
1644 "Failed to update app channel: {e}"
1645 )),
1646 }
1647 }
1648
1649 "delete" => {
1650 let channel_id_str = match require_str(&arguments, "channel_id") {
1651 Ok(s) => s,
1652 Err(e) => return e,
1653 };
1654 let channel_id = match channel_id_str.parse::<crate::typed_id::AppChannelId>() {
1655 Ok(id) => id,
1656 Err(_) => {
1657 return ToolExecutionResult::tool_error(format!(
1658 "Invalid channel_id: {channel_id_str}"
1659 ));
1660 }
1661 };
1662 match store.delete_app_channel(app_id, channel_id).await {
1663 Ok(()) => ToolExecutionResult::success(json!({
1664 "app_id": app_id_str,
1665 "channel_id": channel_id_str,
1666 "ui_link": format!("{}/apps/{}", base_url, app_id),
1667 "message": "App channel deleted successfully"
1668 })),
1669 Err(e) => ToolExecutionResult::tool_error(format!(
1670 "Failed to delete app channel: {e}"
1671 )),
1672 }
1673 }
1674
1675 _ => ToolExecutionResult::tool_error(format!(
1676 "Unknown operation: {operation}. Valid: add, update, delete"
1677 )),
1678 }
1679 }
1680
1681 fn requires_context(&self) -> bool {
1682 true
1683 }
1684}
1685
1686pub struct ReadSessionsTool;
1691
1692#[async_trait]
1693impl Tool for ReadSessionsTool {
1694 fn name(&self) -> &str {
1695 "read_sessions"
1696 }
1697
1698 fn display_name(&self) -> Option<&str> {
1699 Some("Read Sessions")
1700 }
1701
1702 fn description(&self) -> &str {
1703 "Read sessions by ID or list/filter. When id is provided returns a single session; otherwise returns a filtered list."
1704 }
1705
1706 fn parameters_schema(&self) -> Value {
1707 json!({
1708 "type": "object",
1709 "properties": {
1710 "id": {
1711 "type": "string",
1712 "description": "Optional session ID to get a single session"
1713 },
1714 "agent_id": {
1715 "type": "string",
1716 "description": "Optional filter by agent"
1717 },
1718 "limit": {
1719 "type": "integer",
1720 "description": "Optional max results for list (default: 20)"
1721 }
1722 },
1723 "additionalProperties": false
1724 })
1725 }
1726
1727 fn hints(&self) -> ToolHints {
1728 ToolHints::default()
1729 .with_readonly(true)
1730 .with_idempotent(true)
1731 }
1732
1733 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1734 ToolExecutionResult::tool_error(
1735 "read_sessions requires context. This tool must be executed with session context.",
1736 )
1737 }
1738
1739 async fn execute_with_context(
1740 &self,
1741 arguments: Value,
1742 context: &ToolContext,
1743 ) -> ToolExecutionResult {
1744 let store = match get_platform_store(context) {
1745 Ok(s) => s,
1746 Err(e) => return e,
1747 };
1748
1749 let base_url = store.base_url();
1750
1751 if let Some(id_str) = get_str(&arguments, "id") {
1752 let id = match id_str.parse::<crate::typed_id::SessionId>() {
1753 Ok(id) => id,
1754 Err(_) => {
1755 return ToolExecutionResult::tool_error(format!(
1756 "Invalid session id: {id_str}"
1757 ));
1758 }
1759 };
1760 match store.get_session_by_id(id).await {
1761 Ok(Some(s)) => ToolExecutionResult::success(json!({
1762 "id": s.id.to_string(),
1763 "organization_id": s.organization_id,
1764 "title": s.title,
1765 "status": format!("{:?}", s.status),
1766 "agent_id": s.agent_id.as_ref().map(|a| a.to_string()),
1767 "harness_id": s.harness_id.to_string(),
1768 "created_at": s.created_at.to_rfc3339(),
1769 "preview": s.preview,
1770 "output_preview": s.output_preview,
1771 "ui_link": format!("{}/sessions/{}/chat", base_url, s.id),
1772 })),
1773 Ok(None) => ToolExecutionResult::tool_error(format!("Session not found: {id_str}")),
1774 Err(e) => ToolExecutionResult::tool_error(format!("Failed to get session: {e}")),
1775 }
1776 } else {
1777 let limit = arguments
1778 .get("limit")
1779 .and_then(|v| v.as_u64())
1780 .map(|v| v as usize);
1781 let agent_id = get_str(&arguments, "agent_id")
1782 .and_then(|s| s.parse::<crate::typed_id::AgentId>().ok());
1783 match store.list_sessions(limit, agent_id).await {
1784 Ok(sessions) => {
1785 let items: Vec<Value> = sessions
1786 .iter()
1787 .map(|s| {
1788 json!({
1789 "id": s.id.to_string(),
1790 "organization_id": s.organization_id,
1791 "title": s.title,
1792 "status": format!("{:?}", s.status),
1793 "agent_id": s.agent_id.as_ref().map(|a| a.to_string()),
1794 "harness_id": s.harness_id.to_string(),
1795 "created_at": s.created_at.to_rfc3339(),
1796 "preview": s.preview,
1797 "ui_link": format!("{}/sessions/{}/chat", base_url, s.id),
1798 })
1799 })
1800 .collect();
1801 ToolExecutionResult::success(json!({"sessions": items, "count": items.len()}))
1802 }
1803 Err(e) => ToolExecutionResult::tool_error(format!("Failed to list sessions: {e}")),
1804 }
1805 }
1806 }
1807
1808 fn requires_context(&self) -> bool {
1809 true
1810 }
1811}
1812
1813pub struct SessionContextReportTool;
1818
1819#[async_trait]
1820impl Tool for SessionContextReportTool {
1821 fn name(&self) -> &str {
1822 "session_context_report"
1823 }
1824
1825 fn display_name(&self) -> Option<&str> {
1826 Some("Session Context Report")
1827 }
1828
1829 fn description(&self) -> &str {
1830 "Read the latest estimated context token breakdown for a session."
1831 }
1832
1833 fn parameters_schema(&self) -> Value {
1834 json!({
1835 "type": "object",
1836 "properties": {
1837 "session_id": {
1838 "type": "string",
1839 "description": "Session ID to inspect"
1840 }
1841 },
1842 "required": ["session_id"],
1843 "additionalProperties": false
1844 })
1845 }
1846
1847 fn hints(&self) -> ToolHints {
1848 ToolHints::default()
1849 .with_readonly(true)
1850 .with_idempotent(true)
1851 }
1852
1853 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1854 ToolExecutionResult::tool_error(
1855 "session_context_report requires context. This tool must be executed with session context.",
1856 )
1857 }
1858
1859 async fn execute_with_context(
1860 &self,
1861 arguments: Value,
1862 context: &ToolContext,
1863 ) -> ToolExecutionResult {
1864 let store = match get_platform_store(context) {
1865 Ok(s) => s,
1866 Err(e) => return e,
1867 };
1868 let id_str = match require_str(&arguments, "session_id") {
1869 Ok(value) => value,
1870 Err(e) => return e,
1871 };
1872 let id = match id_str.parse::<crate::typed_id::SessionId>() {
1873 Ok(id) => id,
1874 Err(_) => {
1875 return ToolExecutionResult::tool_error(format!("Invalid session_id: {id_str}"));
1876 }
1877 };
1878
1879 match store.get_session_context_report(id).await {
1880 Ok(report) => ToolExecutionResult::success(json!(report)),
1881 Err(e) => ToolExecutionResult::tool_error(format!("Failed to get context report: {e}")),
1882 }
1883 }
1884
1885 fn requires_context(&self) -> bool {
1886 true
1887 }
1888}
1889
1890pub struct ManageSessionsTool;
1895
1896#[async_trait]
1897impl Tool for ManageSessionsTool {
1898 fn name(&self) -> &str {
1899 "manage_sessions"
1900 }
1901
1902 fn display_name(&self) -> Option<&str> {
1903 Some("Manage Sessions")
1904 }
1905
1906 fn description(&self) -> &str {
1907 "Session mutations: create, delete."
1908 }
1909
1910 fn parameters_schema(&self) -> Value {
1911 json!({
1912 "type": "object",
1913 "properties": {
1914 "operation": {
1915 "type": "string",
1916 "enum": ["create", "delete"],
1917 "description": "The mutation to perform"
1918 },
1919 "session_id": {
1920 "type": "string",
1921 "description": "Session ID (required for delete)"
1922 },
1923 "harness_id": {
1924 "type": "string",
1925 "description": "Harness ID for the session. If omitted, uses the org's default (Generic) harness."
1926 },
1927 "agent_id": {
1928 "type": "string",
1929 "description": "Agent ID (optional for create)"
1930 },
1931 "title": {
1932 "type": "string",
1933 "description": "Session title (optional for create)"
1934 },
1935 "locale": {
1936 "type": "string",
1937 "description": "Session locale (optional for create, e.g. uk-UA)"
1938 }
1939 },
1940 "required": ["operation"],
1941 "additionalProperties": false
1942 })
1943 }
1944
1945 fn hints(&self) -> ToolHints {
1946 ToolHints::default().with_narration_noun("session")
1947 }
1948
1949 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1950 ToolExecutionResult::tool_error(
1951 "manage_sessions requires context. This tool must be executed with session context.",
1952 )
1953 }
1954
1955 async fn execute_with_context(
1956 &self,
1957 arguments: Value,
1958 context: &ToolContext,
1959 ) -> ToolExecutionResult {
1960 let store = match get_platform_store(context) {
1961 Ok(s) => s,
1962 Err(e) => return e,
1963 };
1964
1965 let operation = match require_str(&arguments, "operation") {
1966 Ok(op) => op,
1967 Err(e) => return e,
1968 };
1969
1970 let base_url = store.base_url();
1971
1972 match operation {
1973 "create" => {
1974 let harness_id = if let Some(harness_id_str) = get_str(&arguments, "harness_id") {
1975 match harness_id_str.parse::<crate::typed_id::HarnessId>() {
1976 Ok(id) => id,
1977 Err(_) => {
1978 return ToolExecutionResult::tool_error(format!(
1979 "Invalid harness_id: {harness_id_str}"
1980 ));
1981 }
1982 }
1983 } else {
1984 match store.list_harnesses().await {
1986 Ok(harnesses) => {
1987 match harnesses
1988 .iter()
1989 .find(|h| h.is_built_in && h.name == "Generic")
1990 {
1991 Some(h) => h.id,
1992 None => {
1993 return ToolExecutionResult::tool_error(
1994 "No harness_id provided and no default Generic harness found. Please specify a harness_id.",
1995 );
1996 }
1997 }
1998 }
1999 Err(e) => {
2000 return ToolExecutionResult::tool_error(format!(
2001 "No harness_id provided and failed to resolve default harness: {e}"
2002 ));
2003 }
2004 }
2005 };
2006 let agent_id = get_str(&arguments, "agent_id")
2007 .and_then(|s| s.parse::<crate::typed_id::AgentId>().ok());
2008 let title = get_str(&arguments, "title");
2009 let locale = get_str(&arguments, "locale");
2010 match store
2011 .create_session(harness_id, agent_id, title, locale, None, None)
2012 .await
2013 {
2014 Ok(s) => ToolExecutionResult::success(json!({
2015 "id": s.id.to_string(),
2016 "organization_id": s.organization_id,
2017 "title": s.title,
2018 "locale": s.locale,
2019 "status": format!("{:?}", s.status),
2020 "harness_id": s.harness_id.to_string(),
2021 "agent_id": s.agent_id.as_ref().map(|a| a.to_string()),
2022 "ui_link": format!("{}/sessions/{}/chat", base_url, s.id),
2023 "message": "Session created successfully"
2024 })),
2025 Err(e) => {
2026 ToolExecutionResult::tool_error(format!("Failed to create session: {e}"))
2027 }
2028 }
2029 }
2030
2031 "delete" => {
2032 let id_str = match require_str(&arguments, "session_id") {
2033 Ok(s) => s,
2034 Err(e) => return e,
2035 };
2036 let id = match id_str.parse::<crate::typed_id::SessionId>() {
2037 Ok(id) => id,
2038 Err(_) => {
2039 return ToolExecutionResult::tool_error(format!(
2040 "Invalid session_id: {id_str}"
2041 ));
2042 }
2043 };
2044 match store.delete_session(id).await {
2045 Ok(()) => ToolExecutionResult::success(json!({
2046 "session_id": id_str,
2047 "ui_link": format!("{}/sessions/{}/chat", base_url, id_str),
2048 "message": "Session archived successfully"
2049 })),
2050 Err(e) => {
2051 ToolExecutionResult::tool_error(format!("Failed to delete session: {e}"))
2052 }
2053 }
2054 }
2055
2056 _ => ToolExecutionResult::tool_error(format!(
2057 "Unknown operation: {operation}. Valid: create, delete"
2058 )),
2059 }
2060 }
2061
2062 fn requires_context(&self) -> bool {
2063 true
2064 }
2065}
2066
2067pub struct SessionSendMessageTool;
2072
2073#[async_trait]
2074impl Tool for SessionSendMessageTool {
2075 fn name(&self) -> &str {
2076 "session_send_message"
2077 }
2078
2079 fn display_name(&self) -> Option<&str> {
2080 Some("Send Message")
2081 }
2082
2083 fn description(&self) -> &str {
2084 "Send a user message to a session, triggering a turn."
2085 }
2086
2087 fn parameters_schema(&self) -> Value {
2088 json!({
2089 "type": "object",
2090 "properties": {
2091 "session_id": {
2092 "type": "string",
2093 "description": "Target session ID"
2094 },
2095 "content": {
2096 "type": "string",
2097 "description": "Message content"
2098 }
2099 },
2100 "required": ["session_id", "content"],
2101 "additionalProperties": false
2102 })
2103 }
2104
2105 fn hints(&self) -> ToolHints {
2106 ToolHints::default().with_long_running(true)
2107 }
2108
2109 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
2110 ToolExecutionResult::tool_error(
2111 "session_send_message requires context. This tool must be executed with session context.",
2112 )
2113 }
2114
2115 async fn execute_with_context(
2116 &self,
2117 arguments: Value,
2118 context: &ToolContext,
2119 ) -> ToolExecutionResult {
2120 let store = match get_platform_store(context) {
2121 Ok(s) => s,
2122 Err(e) => return e,
2123 };
2124
2125 let session_id_str = match require_str(&arguments, "session_id") {
2126 Ok(s) => s,
2127 Err(e) => return e,
2128 };
2129 let session_id = match session_id_str.parse::<crate::typed_id::SessionId>() {
2130 Ok(id) => id,
2131 Err(_) => {
2132 return ToolExecutionResult::tool_error(format!(
2133 "Invalid session_id: {session_id_str}"
2134 ));
2135 }
2136 };
2137 let content = match require_str(&arguments, "content") {
2138 Ok(s) => s,
2139 Err(e) => return e,
2140 };
2141
2142 let base_url = store.base_url();
2143
2144 match store.send_message(session_id, content).await {
2145 Ok(()) => ToolExecutionResult::success(json!({
2146 "session_id": session_id_str,
2147 "message": "Message sent successfully. Use session_read_response to wait for the agent response.",
2148 "ui_link": format!("{}/sessions/{}/chat", base_url, session_id),
2149 })),
2150 Err(e) => ToolExecutionResult::tool_error(format!("Failed to send message: {e}")),
2151 }
2152 }
2153
2154 fn requires_context(&self) -> bool {
2155 true
2156 }
2157}
2158
2159pub struct SessionReadMessagesTool;
2164
2165#[async_trait]
2166impl Tool for SessionReadMessagesTool {
2167 fn name(&self) -> &str {
2168 "session_read_messages"
2169 }
2170
2171 fn display_name(&self) -> Option<&str> {
2172 Some("Read Messages")
2173 }
2174
2175 fn description(&self) -> &str {
2176 "Read messages from a session."
2177 }
2178
2179 fn parameters_schema(&self) -> Value {
2180 json!({
2181 "type": "object",
2182 "properties": {
2183 "session_id": {
2184 "type": "string",
2185 "description": "Target session ID"
2186 },
2187 "limit": {
2188 "type": "integer",
2189 "description": "Max messages to return. Default: 10, maximum: 50",
2190 "default": SESSION_READ_MESSAGES_DEFAULT_LIMIT,
2191 "minimum": 1,
2192 "maximum": SESSION_READ_MESSAGES_MAX_LIMIT
2193 },
2194 "content_limit": {
2195 "type": "integer",
2196 "description": "Max characters to return per message. Default: 12000, maximum: 50000",
2197 "default": SESSION_READ_MESSAGES_DEFAULT_CONTENT_LIMIT,
2198 "minimum": 1,
2199 "maximum": SESSION_READ_MESSAGES_MAX_CONTENT_LIMIT
2200 }
2201 },
2202 "required": ["session_id"],
2203 "additionalProperties": false
2204 })
2205 }
2206
2207 fn hints(&self) -> ToolHints {
2208 ToolHints::default()
2209 .with_readonly(true)
2210 .with_idempotent(true)
2211 }
2212
2213 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
2214 ToolExecutionResult::tool_error(
2215 "session_read_messages requires context. This tool must be executed with session context.",
2216 )
2217 }
2218
2219 async fn execute_with_context(
2220 &self,
2221 arguments: Value,
2222 context: &ToolContext,
2223 ) -> ToolExecutionResult {
2224 let store = match get_platform_store(context) {
2225 Ok(s) => s,
2226 Err(e) => return e,
2227 };
2228
2229 let session_id_str = match require_str(&arguments, "session_id") {
2230 Ok(s) => s,
2231 Err(e) => return e,
2232 };
2233 let session_id = match session_id_str.parse::<crate::typed_id::SessionId>() {
2234 Ok(id) => id,
2235 Err(_) => {
2236 return ToolExecutionResult::tool_error(format!(
2237 "Invalid session_id: {session_id_str}"
2238 ));
2239 }
2240 };
2241
2242 let limit = match parse_bounded_usize_arg(
2243 &arguments,
2244 "limit",
2245 SESSION_READ_MESSAGES_DEFAULT_LIMIT,
2246 SESSION_READ_MESSAGES_MAX_LIMIT,
2247 ) {
2248 Ok(value) => value,
2249 Err(error) => return error,
2250 };
2251 let content_limit = match parse_bounded_usize_arg(
2252 &arguments,
2253 "content_limit",
2254 SESSION_READ_MESSAGES_DEFAULT_CONTENT_LIMIT,
2255 SESSION_READ_MESSAGES_MAX_CONTENT_LIMIT,
2256 ) {
2257 Ok(value) => value,
2258 Err(error) => return error,
2259 };
2260
2261 let base_url = store.base_url();
2262
2263 match store.get_messages(session_id, Some(limit)).await {
2264 Ok(messages) => {
2265 let items: Vec<Value> = messages
2266 .iter()
2267 .map(|m| {
2268 let (content, truncated, total_chars, returned_chars) =
2269 truncate_content_chars(&m.content, content_limit);
2270 json!({
2271 "role": m.role,
2272 "content": content,
2273 "content_truncated": truncated,
2274 "content_total_chars": total_chars,
2275 "content_returned_chars": returned_chars,
2276 "created_at": m.created_at.to_rfc3339(),
2277 })
2278 })
2279 .collect();
2280 let truncated_message_count = items
2281 .iter()
2282 .filter(|item| item["content_truncated"].as_bool().unwrap_or(false))
2283 .count();
2284 ToolExecutionResult::success(json!({
2285 "messages": items,
2286 "count": items.len(),
2287 "limit": limit,
2288 "content_limit": content_limit,
2289 "truncated_message_count": truncated_message_count,
2290 "session_id": session_id_str,
2291 "ui_link": format!("{}/sessions/{}/chat", base_url, session_id),
2292 }))
2293 }
2294 Err(e) => ToolExecutionResult::tool_error(format!("Failed to get messages: {e}")),
2295 }
2296 }
2297
2298 fn requires_context(&self) -> bool {
2299 true
2300 }
2301}
2302
2303pub struct SessionReadResponseTool;
2308
2309#[async_trait]
2310impl Tool for SessionReadResponseTool {
2311 fn name(&self) -> &str {
2312 "session_read_response"
2313 }
2314
2315 fn display_name(&self) -> Option<&str> {
2316 Some("Read Response")
2317 }
2318
2319 fn description(&self) -> &str {
2320 "Wait for session to finish processing and return the response. Set timeout_secs to 0 to check status without waiting."
2321 }
2322
2323 fn parameters_schema(&self) -> Value {
2324 json!({
2325 "type": "object",
2326 "properties": {
2327 "session_id": {
2328 "type": "string",
2329 "description": "Target session ID"
2330 },
2331 "timeout_secs": {
2332 "type": "integer",
2333 "description": "Optional timeout (default: 120). Set to 0 to check status without waiting."
2334 }
2335 },
2336 "required": ["session_id"],
2337 "additionalProperties": false
2338 })
2339 }
2340
2341 fn hints(&self) -> ToolHints {
2342 ToolHints::default()
2343 .with_readonly(true)
2344 .with_idempotent(true)
2345 .with_long_running(true)
2346 }
2347
2348 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
2349 ToolExecutionResult::tool_error(
2350 "session_read_response requires context. This tool must be executed with session context.",
2351 )
2352 }
2353
2354 async fn execute_with_context(
2355 &self,
2356 arguments: Value,
2357 context: &ToolContext,
2358 ) -> ToolExecutionResult {
2359 let store = match get_platform_store(context) {
2360 Ok(s) => s,
2361 Err(e) => return e,
2362 };
2363
2364 let session_id_str = match require_str(&arguments, "session_id") {
2365 Ok(s) => s,
2366 Err(e) => return e,
2367 };
2368 let session_id = match session_id_str.parse::<crate::typed_id::SessionId>() {
2369 Ok(id) => id,
2370 Err(_) => {
2371 return ToolExecutionResult::tool_error(format!(
2372 "Invalid session_id: {session_id_str}"
2373 ));
2374 }
2375 };
2376
2377 let timeout_secs = arguments.get("timeout_secs").and_then(|v| v.as_u64());
2378 let base_url = store.base_url();
2379
2380 match store.wait_for_idle(session_id, timeout_secs).await {
2381 Ok(status) => ToolExecutionResult::success(json!({
2382 "session_id": session_id_str,
2383 "status": status,
2384 "ui_link": format!("{}/sessions/{}/chat", base_url, session_id),
2385 })),
2386 Err(e) => ToolExecutionResult::tool_error(format!("Failed waiting for response: {e}")),
2387 }
2388 }
2389
2390 fn requires_context(&self) -> bool {
2391 true
2392 }
2393}
2394
2395pub struct ReadCapabilitiesTool;
2400
2401#[async_trait]
2402impl Tool for ReadCapabilitiesTool {
2403 fn name(&self) -> &str {
2404 "read_capabilities"
2405 }
2406
2407 fn display_name(&self) -> Option<&str> {
2408 Some("Read Capabilities")
2409 }
2410
2411 fn description(&self) -> &str {
2412 "Discover available capabilities (built-in, MCP servers, and skills). Use this to find capability IDs before creating or updating agents and harnesses."
2413 }
2414
2415 fn parameters_schema(&self) -> Value {
2416 json!({
2417 "type": "object",
2418 "properties": {
2419 "id": {
2420 "type": "string",
2421 "description": "Optional capability ID to get a single capability"
2422 },
2423 "search": {
2424 "type": "string",
2425 "description": "Optional search query to filter capabilities by name, description, category, or ID (case-insensitive)"
2426 }
2427 },
2428 "additionalProperties": false
2429 })
2430 }
2431
2432 fn hints(&self) -> ToolHints {
2433 ToolHints::default()
2434 .with_readonly(true)
2435 .with_idempotent(true)
2436 }
2437
2438 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
2439 ToolExecutionResult::tool_error(
2440 "read_capabilities requires context. This tool must be executed with session context.",
2441 )
2442 }
2443
2444 async fn execute_with_context(
2445 &self,
2446 arguments: Value,
2447 context: &ToolContext,
2448 ) -> ToolExecutionResult {
2449 let store = match get_platform_store(context) {
2450 Ok(s) => s,
2451 Err(e) => return e,
2452 };
2453 let base_url = store.base_url();
2454
2455 let id_filter = get_str(&arguments, "id");
2456 let search = get_str(&arguments, "search");
2457
2458 let effective_search = id_filter.or(search);
2460
2461 match store.list_capabilities(effective_search).await {
2462 Ok(capabilities) => {
2463 let items: Vec<Value> = capabilities
2464 .iter()
2465 .map(|c| {
2466 let mut item = json!({
2467 "id": c.id.as_str(),
2468 "name": c.name,
2469 "description": c.description,
2470 "status": c.status.to_string(),
2471 "ui_link": format!("{}/capabilities/{}", base_url, c.id.as_str()),
2472 });
2473 if let Some(cat) = &c.category {
2474 item["category"] = json!(cat);
2475 }
2476 if c.is_mcp {
2477 item["type"] = json!("mcp_server");
2478 } else if c.is_skill {
2479 item["type"] = json!("skill");
2480 } else if is_declarative_capability(c.id.as_str()) {
2481 item["type"] = json!("declarative");
2482 } else {
2483 item["type"] = json!("builtin");
2484 }
2485 if !c.tool_definitions.is_empty() {
2486 item["tool_count"] = json!(c.tool_definitions.len());
2487 item["tools"] = json!(
2488 c.tool_definitions
2489 .iter()
2490 .map(|t| t.name())
2491 .collect::<Vec<_>>()
2492 );
2493 }
2494 if !c.dependencies.is_empty() {
2495 item["dependencies"] = json!(c.dependencies);
2496 }
2497 item
2498 })
2499 .collect();
2500
2501 if let Some(target_id) = id_filter {
2503 if let Some(exact) = items.iter().find(|i| i["id"].as_str() == Some(target_id))
2504 {
2505 return ToolExecutionResult::success(exact.clone());
2506 }
2507 return ToolExecutionResult::tool_error(format!(
2508 "Capability not found: {target_id}"
2509 ));
2510 }
2511
2512 let count = items.len();
2513 ToolExecutionResult::success(json!({
2514 "capabilities": items,
2515 "count": count,
2516 "hint": "Use capability IDs when creating or updating agents and harnesses via manage_agents or manage_harnesses (capabilities parameter)."
2517 }))
2518 }
2519 Err(e) => ToolExecutionResult::tool_error(format!("Failed to list capabilities: {e}")),
2520 }
2521 }
2522
2523 fn requires_context(&self) -> bool {
2524 true
2525 }
2526}
2527
2528#[cfg(test)]
2529mod tests {
2530 use super::*;
2531 use crate::platform_store::PlatformStore;
2532 use crate::platform_store::tests::MockPlatformStore;
2533 use crate::typed_id::{AgentId, HarnessId, SessionId};
2534 use std::sync::Arc;
2535
2536 fn mock_context() -> ToolContext {
2537 let store: Arc<dyn PlatformStore> = Arc::new(MockPlatformStore::new());
2538 let mut ctx = ToolContext::new(SessionId::new());
2539 ctx.platform_store = Some(store);
2540 ctx
2541 }
2542
2543 #[test]
2544 fn capability_id_is_platform_management() {
2545 let cap = PlatformManagementCapability;
2546 assert_eq!(cap.id(), "platform_management");
2547 assert_eq!(cap.status(), CapabilityStatus::Available);
2548 }
2549
2550 #[test]
2551 fn capability_provides_fourteen_tools() {
2552 let cap = PlatformManagementCapability;
2553 let tools = cap.tools();
2554 assert_eq!(tools.len(), 14);
2555 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
2556 assert!(names.contains(&"read_capabilities"));
2557 assert!(names.contains(&"read_harnesses"));
2558 assert!(names.contains(&"manage_harnesses"));
2559 assert!(names.contains(&"read_agents"));
2560 assert!(names.contains(&"manage_agents"));
2561 assert!(names.contains(&"read_apps"));
2562 assert!(names.contains(&"manage_apps"));
2563 assert!(names.contains(&"manage_app_channels"));
2564 assert!(names.contains(&"read_sessions"));
2565 assert!(names.contains(&"session_context_report"));
2566 assert!(names.contains(&"manage_sessions"));
2567 assert!(names.contains(&"session_send_message"));
2568 assert!(names.contains(&"session_read_messages"));
2569 assert!(names.contains(&"session_read_response"));
2570 }
2571
2572 #[test]
2573 fn truncate_content_chars_respects_unicode_boundaries() {
2574 let (content, truncated, total_chars, returned_chars) = truncate_content_chars("ab😀cd", 3);
2575
2576 assert_eq!(content, "ab😀");
2577 assert!(truncated);
2578 assert_eq!(total_chars, 5);
2579 assert_eq!(returned_chars, 3);
2580 }
2581
2582 #[tokio::test]
2587 async fn read_harnesses_list_returns_harnesses_with_ui_link() {
2588 let ctx = mock_context();
2589 let tool = ReadHarnessesTool;
2590 let result = tool.execute_with_context(json!({}), &ctx).await;
2591 match result {
2592 ToolExecutionResult::Success(v) => {
2593 assert_eq!(v["count"], 1);
2594 let h = v["harnesses"].as_array().unwrap();
2595 assert!(h[0]["ui_link"].as_str().unwrap().contains("/harnesses/"));
2596 }
2597 other => panic!("expected success, got: {other:?}"),
2598 }
2599 }
2600
2601 #[tokio::test]
2602 async fn read_harnesses_get_by_id_returns_full_detail() {
2603 let ctx = mock_context();
2604 let tool = ReadHarnessesTool;
2605 let result = tool
2606 .execute_with_context(json!({"id": HarnessId::new().to_string()}), &ctx)
2607 .await;
2608 match result {
2609 ToolExecutionResult::Success(v) => {
2610 assert_eq!(v["name"], "test-harness");
2611 assert_eq!(v["display_name"], "Test Harness");
2612 assert!(v["system_prompt"].as_str().is_some());
2613 assert!(v["ui_link"].as_str().unwrap().contains("/harnesses/"));
2614 }
2615 other => panic!("expected success, got: {other:?}"),
2616 }
2617 }
2618
2619 #[tokio::test]
2620 async fn read_harnesses_invalid_id_returns_error() {
2621 let ctx = mock_context();
2622 let tool = ReadHarnessesTool;
2623 let result = tool.execute_with_context(json!({"id": "bad"}), &ctx).await;
2624 match result {
2625 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Invalid harness id")),
2626 other => panic!("expected tool error, got: {other:?}"),
2627 }
2628 }
2629
2630 #[tokio::test]
2635 async fn harness_create_returns_new_harness() {
2636 let ctx = mock_context();
2637 let tool = ManageHarnessesTool;
2638 let result = tool
2639 .execute_with_context(
2640 json!({"operation": "create", "name": "my-harness", "display_name": "My Harness", "system_prompt": "Be fun!"}),
2641 &ctx,
2642 )
2643 .await;
2644 match result {
2645 ToolExecutionResult::Success(v) => {
2646 assert_eq!(v["name"], "my-harness");
2647 assert_eq!(v["display_name"], "My Harness");
2648 assert!(
2649 v["ui_link"]
2650 .as_str()
2651 .unwrap()
2652 .starts_with("http://localhost:9300/harnesses/")
2653 );
2654 }
2655 other => panic!("expected success, got: {other:?}"),
2656 }
2657 }
2658
2659 #[tokio::test]
2660 async fn harness_copy_returns_copied_harness() {
2661 let ctx = mock_context();
2662 let tool = ManageHarnessesTool;
2663 let result = tool
2664 .execute_with_context(
2665 json!({"operation": "copy", "harness_id": HarnessId::new().to_string(), "new_name": "Fun"}),
2666 &ctx,
2667 )
2668 .await;
2669 match result {
2670 ToolExecutionResult::Success(v) => {
2671 assert_eq!(v["name"], "Fun");
2672 assert!(v["message"].as_str().unwrap().contains("copied"));
2673 }
2674 other => panic!("expected success, got: {other:?}"),
2675 }
2676 }
2677
2678 #[tokio::test]
2679 async fn harness_delete_succeeds() {
2680 let ctx = mock_context();
2681 let tool = ManageHarnessesTool;
2682 let result = tool
2683 .execute_with_context(
2684 json!({"operation": "delete", "harness_id": HarnessId::new().to_string()}),
2685 &ctx,
2686 )
2687 .await;
2688 match result {
2689 ToolExecutionResult::Success(v) => {
2690 assert!(v["message"].as_str().unwrap().contains("archived"))
2691 }
2692 other => panic!("expected success, got: {other:?}"),
2693 }
2694 }
2695
2696 #[tokio::test]
2697 async fn harness_invalid_operation_returns_error() {
2698 let ctx = mock_context();
2699 let tool = ManageHarnessesTool;
2700 let result = tool
2701 .execute_with_context(json!({"operation": "explode"}), &ctx)
2702 .await;
2703 match result {
2704 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Unknown operation")),
2705 other => panic!("expected tool error, got: {other:?}"),
2706 }
2707 }
2708
2709 #[tokio::test]
2710 async fn harness_update_succeeds() {
2711 let ctx = mock_context();
2712 let tool = ManageHarnessesTool;
2713 let result = tool
2714 .execute_with_context(
2715 json!({"operation": "update", "harness_id": HarnessId::new().to_string(), "name": "Updated"}),
2716 &ctx,
2717 )
2718 .await;
2719 match result {
2720 ToolExecutionResult::Success(v) => {
2721 assert_eq!(v["name"], "Updated");
2722 assert!(v["message"].as_str().unwrap().contains("updated"));
2723 }
2724 other => panic!("expected success, got: {other:?}"),
2725 }
2726 }
2727
2728 #[tokio::test]
2729 async fn harness_missing_required_param_returns_error() {
2730 let ctx = mock_context();
2731 let tool = ManageHarnessesTool;
2732 let result = tool
2733 .execute_with_context(json!({"operation": "create"}), &ctx)
2734 .await;
2735 match result {
2736 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Missing required")),
2737 other => panic!("expected tool error, got: {other:?}"),
2738 }
2739 }
2740
2741 #[tokio::test]
2746 async fn read_agents_list_returns_agents() {
2747 let ctx = mock_context();
2748 let tool = ReadAgentsTool;
2749 let result = tool.execute_with_context(json!({}), &ctx).await;
2750 match result {
2751 ToolExecutionResult::Success(v) => {
2752 assert_eq!(v["count"], 1);
2753 assert!(
2754 v["agents"].as_array().unwrap()[0]["ui_link"]
2755 .as_str()
2756 .unwrap()
2757 .contains("/agents/")
2758 );
2759 }
2760 other => panic!("expected success, got: {other:?}"),
2761 }
2762 }
2763
2764 #[tokio::test]
2765 async fn read_agents_get_by_id_succeeds() {
2766 let ctx = mock_context();
2767 let tool = ReadAgentsTool;
2768 let result = tool
2769 .execute_with_context(json!({"id": AgentId::new().to_string()}), &ctx)
2770 .await;
2771 match result {
2772 ToolExecutionResult::Success(v) => {
2773 assert_eq!(v["name"], "test-agent");
2774 assert_eq!(v["display_name"], "Test Agent");
2775 assert!(v["ui_link"].as_str().unwrap().contains("/agents/"));
2776 }
2777 other => panic!("expected success, got: {other:?}"),
2778 }
2779 }
2780
2781 #[tokio::test]
2782 async fn read_agents_invalid_id_returns_error() {
2783 let ctx = mock_context();
2784 let tool = ReadAgentsTool;
2785 let result = tool
2786 .execute_with_context(json!({"id": "not-valid"}), &ctx)
2787 .await;
2788 match result {
2789 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Invalid agent id")),
2790 other => panic!("expected tool error, got: {other:?}"),
2791 }
2792 }
2793
2794 #[tokio::test]
2799 async fn agent_create_returns_new_agent() {
2800 let ctx = mock_context();
2801 let tool = ManageAgentsTool;
2802 let result = tool
2803 .execute_with_context(
2804 json!({"operation": "create", "name": "new-agent", "system_prompt": "Be helpful"}),
2805 &ctx,
2806 )
2807 .await;
2808 match result {
2809 ToolExecutionResult::Success(v) => assert_eq!(v["name"], "new-agent"),
2810 other => panic!("expected success, got: {other:?}"),
2811 }
2812 }
2813
2814 #[tokio::test]
2815 async fn agent_create_rejects_non_slug_name() {
2816 let ctx = mock_context();
2817 let tool = ManageAgentsTool;
2818 let result = tool
2819 .execute_with_context(
2820 json!({"operation": "create", "name": "Bad Agent Name", "system_prompt": "hi"}),
2821 &ctx,
2822 )
2823 .await;
2824 match result {
2825 ToolExecutionResult::ToolError(_) => {} other => panic!("expected tool error for non-slug name, got: {other:?}"),
2827 }
2828 }
2829
2830 #[tokio::test]
2831 async fn agent_create_with_display_name() {
2832 let ctx = mock_context();
2833 let tool = ManageAgentsTool;
2834 let result = tool
2835 .execute_with_context(
2836 json!({"operation": "create", "name": "support-bot", "display_name": "Support Bot", "system_prompt": "hi"}),
2837 &ctx,
2838 )
2839 .await;
2840 match result {
2841 ToolExecutionResult::Success(v) => {
2842 assert_eq!(v["name"], "support-bot");
2843 assert_eq!(v["display_name"], "Support Bot");
2844 }
2845 other => panic!("expected success, got: {other:?}"),
2846 }
2847 }
2848
2849 #[tokio::test]
2850 async fn agent_update_succeeds() {
2851 let ctx = mock_context();
2852 let tool = ManageAgentsTool;
2853 let result = tool
2854 .execute_with_context(
2855 json!({"operation": "update", "agent_id": AgentId::new().to_string(), "name": "renamed-agent"}),
2856 &ctx,
2857 )
2858 .await;
2859 match result {
2860 ToolExecutionResult::Success(v) => {
2861 assert_eq!(v["name"], "renamed-agent");
2862 assert!(v["message"].as_str().unwrap().contains("updated"));
2863 }
2864 other => panic!("expected success, got: {other:?}"),
2865 }
2866 }
2867
2868 #[tokio::test]
2869 async fn agent_delete_succeeds() {
2870 let ctx = mock_context();
2871 let tool = ManageAgentsTool;
2872 let result = tool
2873 .execute_with_context(
2874 json!({"operation": "delete", "agent_id": AgentId::new().to_string()}),
2875 &ctx,
2876 )
2877 .await;
2878 match result {
2879 ToolExecutionResult::Success(v) => {
2880 assert!(v["message"].as_str().unwrap().contains("archived"));
2881 }
2882 other => panic!("expected success, got: {other:?}"),
2883 }
2884 }
2885
2886 #[tokio::test]
2887 async fn agent_invalid_operation_returns_error() {
2888 let ctx = mock_context();
2889 let tool = ManageAgentsTool;
2890 let result = tool
2891 .execute_with_context(json!({"operation": "clone"}), &ctx)
2892 .await;
2893 match result {
2894 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Unknown operation")),
2895 other => panic!("expected tool error, got: {other:?}"),
2896 }
2897 }
2898
2899 #[tokio::test]
2900 async fn agent_create_missing_name_returns_error() {
2901 let ctx = mock_context();
2902 let tool = ManageAgentsTool;
2903 let result = tool
2904 .execute_with_context(
2905 json!({"operation": "create", "system_prompt": "test"}),
2906 &ctx,
2907 )
2908 .await;
2909 match result {
2910 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Missing required")),
2911 other => panic!("expected tool error, got: {other:?}"),
2912 }
2913 }
2914
2915 #[tokio::test]
2920 async fn read_apps_list_returns_apps() {
2921 let ctx = mock_context();
2922 let tool = ReadAppsTool;
2923 let result = tool.execute_with_context(json!({}), &ctx).await;
2924 match result {
2925 ToolExecutionResult::Success(v) => {
2926 assert_eq!(v["count"], 1);
2927 assert!(
2928 v["apps"].as_array().unwrap()[0]["ui_link"]
2929 .as_str()
2930 .unwrap()
2931 .contains("/apps/")
2932 );
2933 }
2934 other => panic!("expected success, got: {other:?}"),
2935 }
2936 }
2937
2938 #[tokio::test]
2939 async fn read_apps_get_by_id_returns_channels() {
2940 let ctx = mock_context();
2941 let tool = ReadAppsTool;
2942 let result = tool
2943 .execute_with_context(json!({"id": crate::AppId::new().to_string()}), &ctx)
2944 .await;
2945 match result {
2946 ToolExecutionResult::Success(v) => {
2947 assert_eq!(v["name"], "test-app");
2948 assert_eq!(v["channels"].as_array().unwrap().len(), 1);
2949 }
2950 other => panic!("expected success, got: {other:?}"),
2951 }
2952 }
2953
2954 #[tokio::test]
2955 async fn read_apps_invalid_id_returns_error() {
2956 let ctx = mock_context();
2957 let tool = ReadAppsTool;
2958 let result = tool.execute_with_context(json!({"id": "bad"}), &ctx).await;
2959 match result {
2960 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Invalid app id")),
2961 other => panic!("expected tool error, got: {other:?}"),
2962 }
2963 }
2964
2965 #[tokio::test]
2970 async fn manage_apps_create_returns_new_app() {
2971 let ctx = mock_context();
2972 let tool = ManageAppsTool;
2973 let result = tool
2974 .execute_with_context(
2975 json!({
2976 "operation": "create",
2977 "name": "repo-checker",
2978 "harness_id": HarnessId::new().to_string(),
2979 "channel_type": "schedule",
2980 "channel_config": {
2981 "cron_expression": "0 * * * * * *",
2982 "timezone": "UTC",
2983 "message": "run checks"
2984 }
2985 }),
2986 &ctx,
2987 )
2988 .await;
2989 match result {
2990 ToolExecutionResult::Success(v) => {
2991 assert_eq!(v["name"], "repo-checker");
2992 assert_eq!(v["channels"].as_array().unwrap().len(), 1);
2993 }
2994 other => panic!("expected success, got: {other:?}"),
2995 }
2996 }
2997
2998 #[tokio::test]
2999 async fn manage_apps_publish_returns_published_app() {
3000 let ctx = mock_context();
3001 let tool = ManageAppsTool;
3002 let result = tool
3003 .execute_with_context(
3004 json!({"operation": "publish", "app_id": crate::AppId::new().to_string()}),
3005 &ctx,
3006 )
3007 .await;
3008 match result {
3009 ToolExecutionResult::Success(v) => assert_eq!(v["status"], "published"),
3010 other => panic!("expected success, got: {other:?}"),
3011 }
3012 }
3013
3014 #[tokio::test]
3015 async fn manage_apps_update_accepts_null_agent_identity() {
3016 let ctx = mock_context();
3017 let tool = ManageAppsTool;
3018 let result = tool
3019 .execute_with_context(
3020 json!({
3021 "operation": "update",
3022 "app_id": crate::AppId::new().to_string(),
3023 "agent_identity_id": null
3024 }),
3025 &ctx,
3026 )
3027 .await;
3028 match result {
3029 ToolExecutionResult::Success(v) => assert!(v["agent_identity_id"].is_null()),
3030 other => panic!("expected success, got: {other:?}"),
3031 }
3032 }
3033
3034 #[tokio::test]
3039 async fn manage_app_channels_add_returns_channel() {
3040 let ctx = mock_context();
3041 let tool = ManageAppChannelsTool;
3042 let result = tool
3043 .execute_with_context(
3044 json!({
3045 "operation": "add",
3046 "app_id": crate::AppId::new().to_string(),
3047 "channel_type": "webhook",
3048 "channel_config": {
3049 "token": "secret-1",
3050 "message": "process payload"
3051 }
3052 }),
3053 &ctx,
3054 )
3055 .await;
3056 match result {
3057 ToolExecutionResult::Success(v) => {
3058 assert_eq!(v["channel"]["channel_type"], "webhook");
3059 }
3060 other => panic!("expected success, got: {other:?}"),
3061 }
3062 }
3063
3064 #[tokio::test]
3065 async fn manage_app_channels_delete_succeeds() {
3066 let ctx = mock_context();
3067 let tool = ManageAppChannelsTool;
3068 let result = tool
3069 .execute_with_context(
3070 json!({
3071 "operation": "delete",
3072 "app_id": crate::AppId::new().to_string(),
3073 "channel_id": crate::AppChannelId::new().to_string()
3074 }),
3075 &ctx,
3076 )
3077 .await;
3078 match result {
3079 ToolExecutionResult::Success(v) => {
3080 assert!(v["message"].as_str().unwrap().contains("deleted"));
3081 }
3082 other => panic!("expected success, got: {other:?}"),
3083 }
3084 }
3085
3086 #[tokio::test]
3087 async fn manage_app_channels_invalid_channel_type_returns_error() {
3088 let ctx = mock_context();
3089 let tool = ManageAppChannelsTool;
3090 let result = tool
3091 .execute_with_context(
3092 json!({
3093 "operation": "add",
3094 "app_id": crate::AppId::new().to_string(),
3095 "channel_type": "pagerduty"
3096 }),
3097 &ctx,
3098 )
3099 .await;
3100 match result {
3101 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Invalid channel_type")),
3102 other => panic!("expected tool error, got: {other:?}"),
3103 }
3104 }
3105
3106 #[tokio::test]
3111 async fn read_sessions_list_returns_sessions() {
3112 let ctx = mock_context();
3113 let tool = ReadSessionsTool;
3114 let result = tool.execute_with_context(json!({}), &ctx).await;
3115 match result {
3116 ToolExecutionResult::Success(v) => {
3117 assert_eq!(v["count"], 1);
3118 assert!(
3119 v["sessions"].as_array().unwrap()[0]["ui_link"]
3120 .as_str()
3121 .unwrap()
3122 .contains("/chat")
3123 );
3124 }
3125 other => panic!("expected success, got: {other:?}"),
3126 }
3127 }
3128
3129 #[tokio::test]
3130 async fn read_sessions_get_by_id_succeeds() {
3131 let ctx = mock_context();
3132 let tool = ReadSessionsTool;
3133 let result = tool
3134 .execute_with_context(json!({"id": SessionId::new().to_string()}), &ctx)
3135 .await;
3136 match result {
3137 ToolExecutionResult::Success(v) => {
3138 assert_eq!(v["title"], "Test Session");
3139 assert!(v["ui_link"].as_str().unwrap().contains("/chat"));
3140 }
3141 other => panic!("expected success, got: {other:?}"),
3142 }
3143 }
3144
3145 #[tokio::test]
3146 async fn read_sessions_invalid_id_returns_error() {
3147 let ctx = mock_context();
3148 let tool = ReadSessionsTool;
3149 let result = tool.execute_with_context(json!({"id": "nope"}), &ctx).await;
3150 match result {
3151 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Invalid session id")),
3152 other => panic!("expected tool error, got: {other:?}"),
3153 }
3154 }
3155
3156 #[tokio::test]
3157 async fn session_context_report_returns_report() {
3158 let ctx = mock_context();
3159 let tool = SessionContextReportTool;
3160 let session_id = SessionId::new().to_string();
3161 let result = tool
3162 .execute_with_context(json!({"session_id": session_id}), &ctx)
3163 .await;
3164 match result {
3165 ToolExecutionResult::Success(value) => {
3166 assert_eq!(value["estimated_input_tokens"], 42);
3167 assert_eq!(value["sections"][0]["key"], "conversation");
3168 }
3169 other => panic!("expected success, got: {other:?}"),
3170 }
3171 }
3172
3173 #[tokio::test]
3178 async fn session_create_returns_new_session() {
3179 let ctx = mock_context();
3180 let tool = ManageSessionsTool;
3181 let result = tool
3182 .execute_with_context(
3183 json!({"operation": "create", "harness_id": HarnessId::new().to_string(), "title": "My Session"}),
3184 &ctx,
3185 )
3186 .await;
3187 match result {
3188 ToolExecutionResult::Success(v) => {
3189 assert!(v["ui_link"].as_str().unwrap().contains("/chat"))
3190 }
3191 other => panic!("expected success, got: {other:?}"),
3192 }
3193 }
3194
3195 #[tokio::test]
3196 async fn session_delete_succeeds() {
3197 let ctx = mock_context();
3198 let tool = ManageSessionsTool;
3199 let result = tool
3200 .execute_with_context(
3201 json!({"operation": "delete", "session_id": SessionId::new().to_string()}),
3202 &ctx,
3203 )
3204 .await;
3205 match result {
3206 ToolExecutionResult::Success(v) => {
3207 assert!(v["message"].as_str().unwrap().contains("archived"));
3208 }
3209 other => panic!("expected success, got: {other:?}"),
3210 }
3211 }
3212
3213 #[tokio::test]
3214 async fn session_invalid_operation_returns_error() {
3215 let ctx = mock_context();
3216 let tool = ManageSessionsTool;
3217 let result = tool
3218 .execute_with_context(json!({"operation": "update"}), &ctx)
3219 .await;
3220 match result {
3221 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Unknown operation")),
3222 other => panic!("expected tool error, got: {other:?}"),
3223 }
3224 }
3225
3226 #[tokio::test]
3227 async fn session_create_missing_harness_id_falls_back_to_generic() {
3228 let ctx = mock_context();
3229 let tool = ManageSessionsTool;
3230 let result = tool
3232 .execute_with_context(json!({"operation": "create"}), &ctx)
3233 .await;
3234 match result {
3235 ToolExecutionResult::ToolError(msg) => {
3236 assert!(msg.contains("no default Generic harness found"))
3237 }
3238 other => panic!("expected tool error for missing Generic harness, got: {other:?}"),
3239 }
3240 }
3241
3242 #[tokio::test]
3247 async fn send_message_succeeds() {
3248 let ctx = mock_context();
3249 let tool = SessionSendMessageTool;
3250 let result = tool
3251 .execute_with_context(
3252 json!({"session_id": SessionId::new().to_string(), "content": "Hi!"}),
3253 &ctx,
3254 )
3255 .await;
3256 match result {
3257 ToolExecutionResult::Success(v) => {
3258 assert!(v["message"].as_str().unwrap().contains("sent"))
3259 }
3260 other => panic!("expected success, got: {other:?}"),
3261 }
3262 }
3263
3264 #[tokio::test]
3265 async fn send_message_missing_content_returns_error() {
3266 let ctx = mock_context();
3267 let tool = SessionSendMessageTool;
3268 let result = tool
3269 .execute_with_context(json!({"session_id": SessionId::new().to_string()}), &ctx)
3270 .await;
3271 match result {
3272 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Missing required")),
3273 other => panic!("expected tool error, got: {other:?}"),
3274 }
3275 }
3276
3277 #[tokio::test]
3278 async fn send_message_invalid_session_id_returns_error() {
3279 let ctx = mock_context();
3280 let tool = SessionSendMessageTool;
3281 let result = tool
3282 .execute_with_context(json!({"session_id": "bad-id", "content": "Hi!"}), &ctx)
3283 .await;
3284 match result {
3285 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Invalid session_id")),
3286 other => panic!("expected tool error, got: {other:?}"),
3287 }
3288 }
3289
3290 #[tokio::test]
3295 async fn read_messages_returns_messages() {
3296 let ctx = mock_context();
3297 let tool = SessionReadMessagesTool;
3298 let result = tool
3299 .execute_with_context(
3300 json!({"session_id": SessionId::new().to_string(), "limit": 5}),
3301 &ctx,
3302 )
3303 .await;
3304 match result {
3305 ToolExecutionResult::Success(v) => {
3306 assert_eq!(v["count"], 2);
3307 let msgs = v["messages"].as_array().unwrap();
3308 assert_eq!(msgs[0]["role"], "user");
3309 assert_eq!(msgs[1]["role"], "agent");
3310 }
3311 other => panic!("expected success, got: {other:?}"),
3312 }
3313 }
3314
3315 #[tokio::test]
3316 async fn read_messages_applies_content_limit() {
3317 let ctx = mock_context();
3318 let tool = SessionReadMessagesTool;
3319 let result = tool
3320 .execute_with_context(
3321 json!({"session_id": SessionId::new().to_string(), "content_limit": 2}),
3322 &ctx,
3323 )
3324 .await;
3325 match result {
3326 ToolExecutionResult::Success(v) => {
3327 assert_eq!(v["content_limit"], 2);
3328 assert_eq!(v["truncated_message_count"], 2);
3329 let msgs = v["messages"].as_array().unwrap();
3330 assert_eq!(msgs[0]["content"], "He");
3331 assert_eq!(msgs[0]["content_truncated"], true);
3332 assert_eq!(msgs[0]["content_total_chars"], 5);
3333 assert_eq!(msgs[0]["content_returned_chars"], 2);
3334 }
3335 other => panic!("expected success, got: {other:?}"),
3336 }
3337 }
3338
3339 #[tokio::test]
3340 async fn read_messages_rejects_zero_limits() {
3341 let ctx = mock_context();
3342 let tool = SessionReadMessagesTool;
3343 let result = tool
3344 .execute_with_context(
3345 json!({"session_id": SessionId::new().to_string(), "limit": 0}),
3346 &ctx,
3347 )
3348 .await;
3349 match result {
3350 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("greater than 0")),
3351 other => panic!("expected tool error, got: {other:?}"),
3352 }
3353 }
3354
3355 #[tokio::test]
3356 async fn read_messages_invalid_session_id_returns_error() {
3357 let ctx = mock_context();
3358 let tool = SessionReadMessagesTool;
3359 let result = tool
3360 .execute_with_context(json!({"session_id": "bad-id"}), &ctx)
3361 .await;
3362 match result {
3363 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Invalid session_id")),
3364 other => panic!("expected tool error, got: {other:?}"),
3365 }
3366 }
3367
3368 #[tokio::test]
3369 async fn read_messages_missing_session_id_returns_error() {
3370 let ctx = mock_context();
3371 let tool = SessionReadMessagesTool;
3372 let result = tool.execute_with_context(json!({}), &ctx).await;
3373 match result {
3374 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("Missing required")),
3375 other => panic!("expected tool error, got: {other:?}"),
3376 }
3377 }
3378
3379 #[tokio::test]
3384 async fn read_response_succeeds() {
3385 let ctx = mock_context();
3386 let tool = SessionReadResponseTool;
3387 let result = tool
3388 .execute_with_context(json!({"session_id": SessionId::new().to_string()}), &ctx)
3389 .await;
3390 match result {
3391 ToolExecutionResult::Success(v) => assert_eq!(v["status"], "idle"),
3392 other => panic!("expected success, got: {other:?}"),
3393 }
3394 }
3395
3396 #[tokio::test]
3401 async fn tool_without_context_returns_error() {
3402 let tool = ManageHarnessesTool;
3403 let result = tool.execute(json!({"operation": "create"})).await;
3404 match result {
3405 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("requires context")),
3406 other => panic!("expected tool error, got: {other:?}"),
3407 }
3408 }
3409
3410 #[tokio::test]
3411 async fn tool_without_platform_store_returns_error() {
3412 let ctx = ToolContext::new(SessionId::new());
3413 let tool = ReadHarnessesTool;
3414 let result = tool.execute_with_context(json!({}), &ctx).await;
3415 match result {
3416 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("not available")),
3417 other => panic!("expected tool error, got: {other:?}"),
3418 }
3419 }
3420
3421 #[tokio::test]
3422 async fn missing_operation_returns_error() {
3423 let ctx = mock_context();
3424 let tool = ManageHarnessesTool;
3425 let result = tool.execute_with_context(json!({}), &ctx).await;
3426 match result {
3427 ToolExecutionResult::ToolError(msg) => assert!(msg.contains("operation")),
3428 other => panic!("expected tool error, got: {other:?}"),
3429 }
3430 }
3431
3432 #[tokio::test]
3433 async fn all_tools_require_context() {
3434 assert!(ReadCapabilitiesTool.requires_context());
3435 assert!(ReadHarnessesTool.requires_context());
3436 assert!(ManageHarnessesTool.requires_context());
3437 assert!(ReadAgentsTool.requires_context());
3438 assert!(ManageAgentsTool.requires_context());
3439 assert!(ReadAppsTool.requires_context());
3440 assert!(ManageAppsTool.requires_context());
3441 assert!(ManageAppChannelsTool.requires_context());
3442 assert!(ReadSessionsTool.requires_context());
3443 assert!(SessionContextReportTool.requires_context());
3444 assert!(ManageSessionsTool.requires_context());
3445 assert!(SessionSendMessageTool.requires_context());
3446 assert!(SessionReadMessagesTool.requires_context());
3447 assert!(SessionReadResponseTool.requires_context());
3448 }
3449
3450 #[tokio::test]
3451 async fn all_tools_without_context_return_error() {
3452 for tool_name in [
3454 "read_capabilities",
3455 "read_harnesses",
3456 "manage_harnesses",
3457 "read_agents",
3458 "manage_agents",
3459 "read_apps",
3460 "manage_apps",
3461 "manage_app_channels",
3462 "read_sessions",
3463 "session_context_report",
3464 "manage_sessions",
3465 "session_send_message",
3466 "session_read_messages",
3467 "session_read_response",
3468 ] {
3469 let result = match tool_name {
3470 "read_capabilities" => ReadCapabilitiesTool.execute(json!({})).await,
3471 "read_harnesses" => ReadHarnessesTool.execute(json!({})).await,
3472 "manage_harnesses" => {
3473 ManageHarnessesTool
3474 .execute(json!({"operation": "create"}))
3475 .await
3476 }
3477 "read_agents" => ReadAgentsTool.execute(json!({})).await,
3478 "manage_agents" => {
3479 ManageAgentsTool
3480 .execute(json!({"operation": "create"}))
3481 .await
3482 }
3483 "read_apps" => ReadAppsTool.execute(json!({})).await,
3484 "manage_apps" => ManageAppsTool.execute(json!({"operation": "create"})).await,
3485 "manage_app_channels" => {
3486 ManageAppChannelsTool
3487 .execute(json!({"operation": "add", "app_id": "app_1"}))
3488 .await
3489 }
3490 "read_sessions" => ReadSessionsTool.execute(json!({})).await,
3491 "session_context_report" => {
3492 SessionContextReportTool
3493 .execute(json!({"session_id": "x"}))
3494 .await
3495 }
3496 "manage_sessions" => {
3497 ManageSessionsTool
3498 .execute(json!({"operation": "create"}))
3499 .await
3500 }
3501 "session_send_message" => {
3502 SessionSendMessageTool
3503 .execute(json!({"session_id": "x", "content": "hi"}))
3504 .await
3505 }
3506 "session_read_messages" => {
3507 SessionReadMessagesTool
3508 .execute(json!({"session_id": "x"}))
3509 .await
3510 }
3511 "session_read_response" => {
3512 SessionReadResponseTool
3513 .execute(json!({"session_id": "x"}))
3514 .await
3515 }
3516 _ => unreachable!(),
3517 };
3518 match result {
3519 ToolExecutionResult::ToolError(msg) => {
3520 assert!(msg.contains("requires context"), "tool {tool_name}: {msg}");
3521 }
3522 other => panic!("{tool_name}: expected tool error, got: {other:?}"),
3523 }
3524 }
3525 }
3526
3527 #[tokio::test]
3532 async fn read_capabilities_returns_all() {
3533 let ctx = mock_context();
3534 let tool = ReadCapabilitiesTool;
3535 let result = tool.execute_with_context(json!({}), &ctx).await;
3536 match result {
3537 ToolExecutionResult::Success(v) => {
3538 let count = v["count"].as_u64().unwrap();
3539 assert!(count > 0, "should return at least one capability");
3540 let caps = v["capabilities"].as_array().unwrap();
3541 for cap in caps {
3542 assert!(cap["id"].is_string());
3543 assert!(cap["name"].is_string());
3544 assert!(cap["type"].is_string());
3545 assert!(cap["ui_link"].as_str().unwrap().contains("/capabilities/"));
3546 }
3547 assert!(v["hint"].as_str().unwrap().contains("capability IDs"));
3548 }
3549 other => panic!("expected success, got: {other:?}"),
3550 }
3551 }
3552
3553 #[tokio::test]
3554 async fn read_capabilities_search_filters_results() {
3555 let ctx = mock_context();
3556 let tool = ReadCapabilitiesTool;
3557 let result = tool
3558 .execute_with_context(json!({"search": "current_time"}), &ctx)
3559 .await;
3560 match result {
3561 ToolExecutionResult::Success(v) => {
3562 let count = v["count"].as_u64().unwrap();
3563 assert!(count >= 1, "should find at least current_time");
3564 let caps = v["capabilities"].as_array().unwrap();
3565 assert!(
3566 caps.iter()
3567 .any(|c| c["id"].as_str().unwrap() == "current_time"),
3568 "should contain current_time"
3569 );
3570 }
3571 other => panic!("expected success, got: {other:?}"),
3572 }
3573 }
3574
3575 #[tokio::test]
3576 async fn read_capabilities_search_no_match() {
3577 let ctx = mock_context();
3578 let tool = ReadCapabilitiesTool;
3579 let result = tool
3580 .execute_with_context(json!({"search": "zzz_nonexistent_zzz"}), &ctx)
3581 .await;
3582 match result {
3583 ToolExecutionResult::Success(v) => {
3584 assert_eq!(v["count"], 0);
3585 }
3586 other => panic!("expected success, got: {other:?}"),
3587 }
3588 }
3589
3590 #[tokio::test]
3591 async fn read_capabilities_empty_id_returns_all() {
3592 let ctx = mock_context();
3593 let tool = ReadCapabilitiesTool;
3594 let result = tool
3596 .execute_with_context(json!({"id": "", "search": ""}), &ctx)
3597 .await;
3598 match result {
3599 ToolExecutionResult::Success(v) => {
3600 let count = v["count"].as_u64().unwrap();
3601 assert!(count > 0, "empty id/search should return all capabilities");
3602 }
3603 other => panic!("expected success with all capabilities, got: {other:?}"),
3604 }
3605 }
3606
3607 #[tokio::test]
3608 async fn read_capabilities_empty_id_only_returns_all() {
3609 let ctx = mock_context();
3610 let tool = ReadCapabilitiesTool;
3611 let result = tool.execute_with_context(json!({"id": ""}), &ctx).await;
3612 match result {
3613 ToolExecutionResult::Success(v) => {
3614 let count = v["count"].as_u64().unwrap();
3615 assert!(count > 0, "empty id should return all capabilities");
3616 }
3617 other => panic!("expected success, got: {other:?}"),
3618 }
3619 }
3620
3621 #[test]
3622 fn capability_has_system_prompt_addition() {
3623 let cap = PlatformManagementCapability;
3624 let prompt = cap.system_prompt_addition().expect("should have prompt");
3625 assert!(prompt.contains("read_capabilities"));
3626 assert!(prompt.contains("Capabilities"));
3627 }
3628}