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