fast-agent-mcp 0.3.13__py3-none-any.whl → 0.3.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (33) hide show
  1. fast_agent/agents/llm_agent.py +14 -33
  2. fast_agent/agents/llm_decorator.py +13 -2
  3. fast_agent/agents/mcp_agent.py +18 -2
  4. fast_agent/agents/tool_agent.py +8 -10
  5. fast_agent/cli/commands/check_config.py +45 -1
  6. fast_agent/config.py +63 -0
  7. fast_agent/constants.py +3 -0
  8. fast_agent/context.py +42 -9
  9. fast_agent/core/logging/listeners.py +1 -1
  10. fast_agent/event_progress.py +2 -3
  11. fast_agent/interfaces.py +9 -2
  12. fast_agent/llm/model_factory.py +4 -0
  13. fast_agent/llm/provider_key_manager.py +1 -0
  14. fast_agent/llm/provider_types.py +1 -0
  15. fast_agent/llm/request_params.py +3 -1
  16. fast_agent/mcp/mcp_aggregator.py +313 -40
  17. fast_agent/mcp/mcp_connection_manager.py +39 -9
  18. fast_agent/mcp/skybridge.py +45 -0
  19. fast_agent/mcp/sse_tracking.py +287 -0
  20. fast_agent/mcp/transport_tracking.py +37 -3
  21. fast_agent/mcp/types.py +24 -0
  22. fast_agent/resources/examples/workflows/router.py +1 -0
  23. fast_agent/resources/setup/fastagent.config.yaml +5 -0
  24. fast_agent/ui/console_display.py +295 -18
  25. fast_agent/ui/enhanced_prompt.py +107 -58
  26. fast_agent/ui/interactive_prompt.py +57 -34
  27. fast_agent/ui/mcp_display.py +108 -27
  28. fast_agent/ui/rich_progress.py +4 -1
  29. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.14.dist-info}/METADATA +2 -2
  30. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.14.dist-info}/RECORD +33 -30
  31. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.14.dist-info}/WHEEL +0 -0
  32. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.14.dist-info}/entry_points.txt +0 -0
  33. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.14.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  from enum import Enum
2
2
  from json import JSONDecodeError
3
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Tuple, Union
4
4
 
5
5
  from mcp.types import CallToolResult
6
6
  from rich.panel import Panel
@@ -18,6 +18,7 @@ from fast_agent.ui.mermaid_utils import (
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
21
+ from fast_agent.mcp.skybridge import SkybridgeServerConfig
21
22
 
22
23
  CODE_STYLE = "native"
23
24
 
@@ -538,8 +539,21 @@ class ConsoleDisplay:
538
539
 
539
540
  return formatted
540
541
 
541
- def show_tool_result(self, result: CallToolResult, name: str | None = None) -> None:
542
- """Display a tool result in the new visual style."""
542
+ def show_tool_result(
543
+ self,
544
+ result: CallToolResult,
545
+ name: str | None = None,
546
+ tool_name: str | None = None,
547
+ skybridge_config: "SkybridgeServerConfig | None" = None,
548
+ ) -> None:
549
+ """Display a tool result in the new visual style.
550
+
551
+ Args:
552
+ result: The tool result to display
553
+ name: Optional agent name
554
+ tool_name: Optional tool name for skybridge detection
555
+ skybridge_config: Optional skybridge configuration for the server
556
+ """
543
557
  if not self.config or not self.config.logger.show_tools:
544
558
  return
545
559
 
@@ -548,6 +562,20 @@ class ConsoleDisplay:
548
562
 
549
563
  # Analyze content to determine display format and status
550
564
  content = result.content
565
+ structured_content = getattr(result, "structuredContent", None)
566
+ has_structured = structured_content is not None
567
+
568
+ # Determine if this is a skybridge tool
569
+ is_skybridge_tool = False
570
+ skybridge_resource_uri = None
571
+ if has_structured and tool_name and skybridge_config:
572
+ # Check if this tool is a valid skybridge tool
573
+ for tool_cfg in skybridge_config.tools:
574
+ if tool_cfg.tool_name == tool_name and tool_cfg.is_valid:
575
+ is_skybridge_tool = True
576
+ skybridge_resource_uri = tool_cfg.resource_uri
577
+ break
578
+
551
579
  if result.isError:
552
580
  status = "ERROR"
553
581
  else:
@@ -575,9 +603,9 @@ class ConsoleDisplay:
575
603
  if channel == "post-json":
576
604
  transport_info = "HTTP (JSON-RPC)"
577
605
  elif channel == "post-sse":
578
- transport_info = "HTTP (SSE)"
606
+ transport_info = "Legacy SSE"
579
607
  elif channel == "get":
580
- transport_info = "HTTP (SSE)"
608
+ transport_info = "Legacy SSE"
581
609
  elif channel == "resumption":
582
610
  transport_info = "Resumption"
583
611
  elif channel == "stdio":
@@ -591,21 +619,138 @@ class ConsoleDisplay:
591
619
  if isinstance(elapsed, (int, float)):
592
620
  bottom_metadata_items.append(self._format_elapsed(float(elapsed)))
593
621
 
622
+ # Add structured content indicator if present
623
+ if has_structured:
624
+ bottom_metadata_items.append("Structured ■")
625
+
594
626
  bottom_metadata = bottom_metadata_items or None
595
627
 
596
628
  # Build right info (without channel info)
597
629
  right_info = f"[dim]tool result - {status}[/dim]"
598
630
 
599
- # Display using unified method
600
- self.display_message(
601
- content=content,
602
- message_type=MessageType.TOOL_RESULT,
603
- name=name,
604
- right_info=right_info,
605
- bottom_metadata=bottom_metadata,
606
- is_error=result.isError,
607
- truncate_content=True,
608
- )
631
+ if has_structured:
632
+ # Handle structured content display manually to insert it before bottom separator
633
+ # Display main content without bottom separator
634
+ config = MESSAGE_CONFIGS[MessageType.TOOL_RESULT]
635
+ block_color = "red" if result.isError else config["block_color"]
636
+ arrow = config["arrow"]
637
+ arrow_style = config["arrow_style"]
638
+ left = f"[{block_color}]▎[/{block_color}][{arrow_style}]{arrow}[/{arrow_style}]"
639
+ if name:
640
+ left += f" [{block_color if not result.isError else 'red'}]{name}[/{block_color if not result.isError else 'red'}]"
641
+
642
+ # Top separator
643
+ self._create_combined_separator_status(left, right_info)
644
+
645
+ # Main content
646
+ self._display_content(
647
+ content, True, result.isError, MessageType.TOOL_RESULT, check_markdown_markers=False
648
+ )
649
+
650
+ # Structured content separator and display
651
+ console.console.print()
652
+ total_width = console.console.size.width
653
+
654
+ if is_skybridge_tool:
655
+ # Skybridge: magenta separator with resource URI
656
+ resource_label = (
657
+ f"skybridge resource: {skybridge_resource_uri}"
658
+ if skybridge_resource_uri
659
+ else "skybridge resource"
660
+ )
661
+ prefix = Text("─| ")
662
+ prefix.stylize("dim")
663
+ resource_text = Text(resource_label, style="magenta")
664
+ suffix = Text(" |")
665
+ suffix.stylize("dim")
666
+
667
+ separator_line = Text()
668
+ separator_line.append_text(prefix)
669
+ separator_line.append_text(resource_text)
670
+ separator_line.append_text(suffix)
671
+ remaining = total_width - separator_line.cell_len
672
+ if remaining > 0:
673
+ separator_line.append("─" * remaining, style="dim")
674
+ console.console.print(separator_line, markup=self._markup)
675
+ console.console.print()
676
+
677
+ # Display with bright syntax highlighting
678
+ import json
679
+
680
+ from rich.syntax import Syntax
681
+
682
+ json_str = json.dumps(structured_content, indent=2)
683
+ syntax_obj = Syntax(json_str, "json", theme=CODE_STYLE, background_color="default")
684
+ console.console.print(syntax_obj, markup=self._markup)
685
+ else:
686
+ # Regular tool: dim separator
687
+ prefix = Text("─| ")
688
+ prefix.stylize("dim")
689
+ label_text = Text("Structured Content", style="dim")
690
+ suffix = Text(" |")
691
+ suffix.stylize("dim")
692
+
693
+ separator_line = Text()
694
+ separator_line.append_text(prefix)
695
+ separator_line.append_text(label_text)
696
+ separator_line.append_text(suffix)
697
+ remaining = total_width - separator_line.cell_len
698
+ if remaining > 0:
699
+ separator_line.append("─" * remaining, style="dim")
700
+ console.console.print(separator_line, markup=self._markup)
701
+ console.console.print()
702
+
703
+ # Display truncated content in dim
704
+ from rich.pretty import Pretty
705
+
706
+ if self.config and self.config.logger.truncate_tools:
707
+ pretty_obj = Pretty(structured_content, max_length=10, max_string=50)
708
+ else:
709
+ pretty_obj = Pretty(structured_content)
710
+ console.console.print(pretty_obj, style="dim", markup=self._markup)
711
+
712
+ # Bottom separator with metadata
713
+ console.console.print()
714
+ if bottom_metadata:
715
+ display_items = (
716
+ self._shorten_items(bottom_metadata, 12) if True else bottom_metadata
717
+ )
718
+ prefix = Text("─| ")
719
+ prefix.stylize("dim")
720
+ suffix = Text(" |")
721
+ suffix.stylize("dim")
722
+ available = max(0, total_width - prefix.cell_len - suffix.cell_len)
723
+
724
+ metadata_text = self._format_bottom_metadata(
725
+ display_items,
726
+ None,
727
+ config["highlight_color"],
728
+ max_width=available,
729
+ )
730
+
731
+ line = Text()
732
+ line.append_text(prefix)
733
+ line.append_text(metadata_text)
734
+ line.append_text(suffix)
735
+ remaining = total_width - line.cell_len
736
+ if remaining > 0:
737
+ line.append("─" * remaining, style="dim")
738
+ console.console.print(line, markup=self._markup)
739
+ else:
740
+ console.console.print("─" * total_width, style="dim")
741
+ console.console.print()
742
+
743
+ else:
744
+ # No structured content - use standard display
745
+ self.display_message(
746
+ content=content,
747
+ message_type=MessageType.TOOL_RESULT,
748
+ name=name,
749
+ right_info=right_info,
750
+ bottom_metadata=bottom_metadata,
751
+ is_error=result.isError,
752
+ truncate_content=True,
753
+ )
609
754
 
610
755
  def show_tool_call(
611
756
  self,
@@ -669,9 +814,7 @@ class ConsoleDisplay:
669
814
  # No active prompt_toolkit session - display with rich as before
670
815
  # Combined separator and status line
671
816
  if agent_name:
672
- left = (
673
- f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
674
- )
817
+ left = f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
675
818
  else:
676
819
  left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
677
820
 
@@ -727,6 +870,140 @@ class ConsoleDisplay:
727
870
  console.console.print(combined, markup=self._markup)
728
871
  console.console.print()
729
872
 
873
+ @staticmethod
874
+ def summarize_skybridge_configs(
875
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
876
+ ) -> Tuple[List[Dict[str, Any]], List[str]]:
877
+ """Convert raw Skybridge configs into display-friendly summary data."""
878
+ server_rows: List[Dict[str, Any]] = []
879
+ warnings: List[str] = []
880
+ warning_seen: Set[str] = set()
881
+
882
+ if not configs:
883
+ return server_rows, warnings
884
+
885
+ def add_warning(message: str) -> None:
886
+ formatted = message.strip()
887
+ if not formatted:
888
+ return
889
+ if formatted not in warning_seen:
890
+ warnings.append(formatted)
891
+ warning_seen.add(formatted)
892
+
893
+ for server_name in sorted(configs.keys()):
894
+ config = configs.get(server_name)
895
+ if not config:
896
+ continue
897
+ resources = list(config.ui_resources or [])
898
+ has_skybridge_signal = bool(
899
+ config.enabled or resources or config.tools or config.warnings
900
+ )
901
+ if not has_skybridge_signal:
902
+ continue
903
+
904
+ valid_resource_count = sum(1 for resource in resources if resource.is_skybridge)
905
+
906
+ server_rows.append(
907
+ {
908
+ "server_name": server_name,
909
+ "config": config,
910
+ "resources": resources,
911
+ "valid_resource_count": valid_resource_count,
912
+ "total_resource_count": len(resources),
913
+ "active_tools": [
914
+ {
915
+ "name": tool.display_name,
916
+ "template": str(tool.template_uri) if tool.template_uri else None,
917
+ }
918
+ for tool in config.tools
919
+ if tool.is_valid
920
+ ],
921
+ "enabled": config.enabled,
922
+ }
923
+ )
924
+
925
+ for warning in config.warnings:
926
+ message = warning.strip()
927
+ if not message:
928
+ continue
929
+ if not message.startswith(server_name):
930
+ message = f"{server_name} {message}"
931
+ add_warning(message)
932
+
933
+ return server_rows, warnings
934
+
935
+ def show_skybridge_summary(
936
+ self,
937
+ agent_name: str,
938
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
939
+ ) -> None:
940
+ """Display Skybridge availability and warnings."""
941
+ server_rows, warnings = self.summarize_skybridge_configs(configs)
942
+
943
+ if not server_rows and not warnings:
944
+ return
945
+
946
+ heading = "[dim]OpenAI Apps SDK ([/dim][cyan]skybridge[/cyan][dim]) detected:[/dim]"
947
+ console.console.print()
948
+ console.console.print(heading, markup=self._markup)
949
+
950
+ if not server_rows:
951
+ console.console.print("[dim] ● none detected[/dim]", markup=self._markup)
952
+ else:
953
+ for row in server_rows:
954
+ server_name = row["server_name"]
955
+ resource_count = row["valid_resource_count"]
956
+ total_resource_count = row["total_resource_count"]
957
+ tool_infos = row["active_tools"]
958
+ enabled = row["enabled"]
959
+
960
+ tool_count = len(tool_infos)
961
+ tool_word = "tool" if tool_count == 1 else "tools"
962
+ resource_word = (
963
+ "skybridge resource" if resource_count == 1 else "skybridge resources"
964
+ )
965
+ tool_segment = f"[cyan]{tool_count}[/cyan][dim] {tool_word}[/dim]"
966
+ resource_segment = f"[cyan]{resource_count}[/cyan][dim] {resource_word}[/dim]"
967
+ name_style = "cyan" if enabled else "yellow"
968
+ status_suffix = "" if enabled else "[dim] (issues detected)[/dim]"
969
+
970
+ console.console.print(
971
+ f"[dim] ● [/dim][{name_style}]{server_name}[/{name_style}]{status_suffix}"
972
+ f"[dim] — [/dim]{tool_segment}[dim], [/dim]{resource_segment}",
973
+ markup=self._markup,
974
+ )
975
+
976
+ for tool_info in tool_infos:
977
+ template_text = (
978
+ f"[dim] ({tool_info['template']})[/dim]" if tool_info["template"] else ""
979
+ )
980
+ console.console.print(
981
+ f"[dim] ▶ [/dim][white]{tool_info['name']}[/white]{template_text}",
982
+ markup=self._markup,
983
+ )
984
+
985
+ if tool_count == 0 and resource_count > 0:
986
+ console.console.print(
987
+ "[dim] ▶ tools not linked[/dim]",
988
+ markup=self._markup,
989
+ )
990
+ if not enabled and total_resource_count > resource_count:
991
+ invalid_count = total_resource_count - resource_count
992
+ invalid_word = "resource" if invalid_count == 1 else "resources"
993
+ console.console.print(
994
+ (
995
+ "[dim] ▶ "
996
+ f"[/dim][cyan]{invalid_count}[/cyan][dim] {invalid_word} detected with non-skybridge MIME type[/dim]"
997
+ ),
998
+ markup=self._markup,
999
+ )
1000
+
1001
+ for warning_entry in warnings:
1002
+ console.console.print(
1003
+ f"[dim red] ▶ [/dim red][red]warning[/red] [dim]{warning_entry}[/dim]",
1004
+ markup=self._markup,
1005
+ )
1006
+
730
1007
  async def show_assistant_message(
731
1008
  self,
732
1009
  message_text: Union[str, Text, "PromptMessageExtended"],
@@ -9,7 +9,7 @@ import shlex
9
9
  import subprocess
10
10
  import tempfile
11
11
  from importlib.metadata import version
12
- from typing import TYPE_CHECKING, List, Optional
12
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
13
13
 
14
14
  from prompt_toolkit import PromptSession
15
15
  from prompt_toolkit.completion import Completer, Completion, WordCompleter
@@ -24,6 +24,7 @@ from fast_agent.agents.agent_types import AgentType
24
24
  from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL, FAST_AGENT_REMOVED_METADATA_CHANNEL
25
25
  from fast_agent.core.exceptions import PromptExitError
26
26
  from fast_agent.llm.model_info import get_model_info
27
+ from fast_agent.mcp.types import McpAgentProtocol
27
28
  from fast_agent.ui.mcp_display import render_mcp_status
28
29
 
29
30
  if TYPE_CHECKING:
@@ -103,10 +104,9 @@ async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp
103
104
 
104
105
  # Get counts TODO -- add this to the type library or adjust the way aggregator/reporting works
105
106
  server_count = 0
106
- if hasattr(agent, "_aggregator") and hasattr(agent._aggregator, "server_names"):
107
- server_count = (
108
- len(agent._aggregator.server_names) if agent._aggregator.server_names else 0
109
- )
107
+ if isinstance(agent, McpAgentProtocol):
108
+ server_names = agent.aggregator.server_names
109
+ server_count = len(server_names) if server_names else 0
110
110
 
111
111
  tools_result = await agent.list_tools()
112
112
  tool_count = (
@@ -182,6 +182,17 @@ async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp
182
182
  rich_print(f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {content}")
183
183
  # await _render_mcp_status(agent)
184
184
 
185
+ # Display Skybridge status (if aggregator discovered any)
186
+ try:
187
+ aggregator = agent.aggregator if isinstance(agent, McpAgentProtocol) else None
188
+ display = getattr(agent, "display", None)
189
+ if aggregator and display and hasattr(display, "show_skybridge_summary"):
190
+ skybridge_configs = await aggregator.get_skybridge_configs()
191
+ display.show_skybridge_summary(agent_name, skybridge_configs)
192
+ except Exception:
193
+ # Ignore Skybridge rendering issues to avoid interfering with startup
194
+ pass
195
+
185
196
  # Mark as shown
186
197
  _agent_info_shown.add(agent_name)
187
198
 
@@ -648,60 +659,96 @@ async def get_enhanced_input(
648
659
  model_display = None
649
660
  tdv_segment = None
650
661
  turn_count = 0
651
- try:
652
- if agent_provider:
662
+ agent = None
663
+ if agent_provider:
664
+ try:
653
665
  agent = agent_provider._agent(agent_name)
666
+ except Exception as exc:
667
+ print(f"[toolbar debug] unable to resolve agent '{agent_name}': {exc}")
654
668
 
655
- # Get turn count from message history
656
- for message in agent.message_history:
657
- if message.role == "user":
658
- turn_count += 1
659
-
660
- # Get model name from LLM
661
- if agent.llm and agent.llm.model_name:
662
- model_name = agent.llm.model_name
663
- # Truncate model name to max 25 characters with ellipsis
664
- max_len = 25
665
- if len(model_name) > max_len:
666
- # Keep total length at max_len including ellipsis
667
- model_display = model_name[: max_len - 1] + "…"
668
- else:
669
- model_display = model_name
670
-
671
- # Build TDV capability segment based on model database
672
- info = get_model_info(agent)
673
- # Default to text-only if info resolution fails for any reason
674
- t, d, v = (True, False, False)
675
- if info:
676
- t, d, v = info.tdv_flags
677
-
678
- # Check for alert flags in user messages
679
- alert_flags: set[str] = set()
680
- error_seen = False
681
- for message in agent.message_history:
682
- if message.channels:
683
- if message.channels.get(FAST_AGENT_ERROR_CHANNEL):
684
- error_seen = True
685
- if message.role == "user" and message.channels:
686
- meta_blocks = message.channels.get(FAST_AGENT_REMOVED_METADATA_CHANNEL, [])
687
- alert_flags.update(_extract_alert_flags_from_meta(meta_blocks))
688
-
689
- if error_seen and not alert_flags:
690
- alert_flags.add("T")
691
-
692
- def _style_flag(letter: str, supported: bool) -> str:
693
- # Enabled uses the same color as NORMAL mode (ansigreen), disabled is dim
694
- if letter in alert_flags:
695
- return f"<style fg='ansired' bg='ansiblack'>{letter}</style>"
696
-
697
- enabled_color = "ansigreen"
698
- if supported:
699
- return f"<style fg='{enabled_color}' bg='ansiblack'>{letter}</style>"
700
- return f"<style fg='ansiblack' bg='ansiwhite'>{letter}</style>"
701
-
702
- tdv_segment = f"{_style_flag('T', t)}{_style_flag('D', d)}{_style_flag('V', v)}"
703
- except Exception:
704
- # If anything goes wrong determining the model, omit it gracefully
669
+ if agent:
670
+ for message in agent.message_history:
671
+ if message.role == "user":
672
+ turn_count += 1
673
+
674
+ # Resolve LLM reference safely (avoid assertion when unattached)
675
+ llm = None
676
+ try:
677
+ llm = agent.llm
678
+ except AssertionError:
679
+ llm = getattr(agent, "_llm", None)
680
+ except Exception as exc:
681
+ print(f"[toolbar debug] agent.llm access failed for '{agent_name}': {exc}")
682
+
683
+ model_name = None
684
+ if llm:
685
+ model_name = getattr(llm, "model_name", None)
686
+ if not model_name:
687
+ model_name = getattr(getattr(llm, "default_request_params", None), "model", None)
688
+
689
+ if not model_name:
690
+ model_name = getattr(agent.config, "model", None)
691
+ if not model_name and getattr(agent.config, "default_request_params", None):
692
+ model_name = getattr(agent.config.default_request_params, "model", None)
693
+ if not model_name:
694
+ context = getattr(agent, "context", None) or getattr(agent_provider, "context", None)
695
+ config_obj = getattr(context, "config", None) if context else None
696
+ model_name = getattr(config_obj, "default_model", None)
697
+
698
+ if model_name:
699
+ max_len = 25
700
+ model_display = model_name[: max_len - 1] + "…" if len(model_name) > max_len else model_name
701
+ else:
702
+ print(f"[toolbar debug] no model resolved for agent '{agent_name}'")
703
+ model_display = "unknown"
704
+
705
+ # Build TDV capability segment based on model database
706
+ info = None
707
+ if llm:
708
+ try:
709
+ info = get_model_info(llm)
710
+ except TypeError:
711
+ info = None
712
+ if not info and model_name:
713
+ try:
714
+ info = get_model_info(model_name)
715
+ except TypeError:
716
+ info = None
717
+ except Exception as exc:
718
+ print(f"[toolbar debug] get_model_info failed for '{agent_name}': {exc}")
719
+ info = None
720
+
721
+ # Default to text-only if info resolution fails for any reason
722
+ t, d, v = (True, False, False)
723
+ if info:
724
+ t, d, v = info.tdv_flags
725
+
726
+ # Check for alert flags in user messages
727
+ alert_flags: set[str] = set()
728
+ error_seen = False
729
+ for message in agent.message_history:
730
+ if message.channels:
731
+ if message.channels.get(FAST_AGENT_ERROR_CHANNEL):
732
+ error_seen = True
733
+ if message.role == "user" and message.channels:
734
+ meta_blocks = message.channels.get(FAST_AGENT_REMOVED_METADATA_CHANNEL, [])
735
+ alert_flags.update(_extract_alert_flags_from_meta(meta_blocks))
736
+
737
+ if error_seen and not alert_flags:
738
+ alert_flags.add("T")
739
+
740
+ def _style_flag(letter: str, supported: bool) -> str:
741
+ # Enabled uses the same color as NORMAL mode (ansigreen), disabled is dim
742
+ if letter in alert_flags:
743
+ return f"<style fg='ansired' bg='ansiblack'>{letter}</style>"
744
+
745
+ enabled_color = "ansigreen"
746
+ if supported:
747
+ return f"<style fg='{enabled_color}' bg='ansiblack'>{letter}</style>"
748
+ return f"<style fg='ansiblack' bg='ansiwhite'>{letter}</style>"
749
+
750
+ tdv_segment = f"{_style_flag('T', t)}{_style_flag('D', d)}{_style_flag('V', v)}"
751
+ else:
705
752
  model_display = None
706
753
  tdv_segment = None
707
754
 
@@ -1022,7 +1069,9 @@ async def get_argument_input(
1022
1069
  prompt_session.app.exit()
1023
1070
 
1024
1071
 
1025
- async def handle_special_commands(command, agent_app=None):
1072
+ async def handle_special_commands(
1073
+ command: Any, agent_app: "AgentApp | None" = None
1074
+ ) -> bool | Dict[str, Any]:
1026
1075
  """
1027
1076
  Handle special input commands.
1028
1077