glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.12__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.
Files changed (127) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +217 -42
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  8. glaip_sdk/cli/commands/agents/_common.py +561 -0
  9. glaip_sdk/cli/commands/agents/create.py +151 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +369 -23
  55. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +79 -5
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1027 -88
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +87 -0
  60. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  61. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  62. glaip_sdk/cli/slash/tui/layouts/harlequin.py +160 -0
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +119 -12
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +374 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +5 -3
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +50 -8
  78. glaip_sdk/client/hitl.py +136 -0
  79. glaip_sdk/client/main.py +7 -1
  80. glaip_sdk/client/mcps.py +44 -13
  81. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  82. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  83. glaip_sdk/client/payloads/agent/responses.py +43 -0
  84. glaip_sdk/client/run_rendering.py +414 -3
  85. glaip_sdk/client/schedules.py +439 -0
  86. glaip_sdk/client/tools.py +57 -26
  87. glaip_sdk/guardrails/__init__.py +80 -0
  88. glaip_sdk/guardrails/serializer.py +89 -0
  89. glaip_sdk/hitl/__init__.py +48 -0
  90. glaip_sdk/hitl/base.py +64 -0
  91. glaip_sdk/hitl/callback.py +43 -0
  92. glaip_sdk/hitl/local.py +121 -0
  93. glaip_sdk/hitl/remote.py +523 -0
  94. glaip_sdk/models/__init__.py +17 -0
  95. glaip_sdk/models/agent_runs.py +2 -1
  96. glaip_sdk/models/schedule.py +224 -0
  97. glaip_sdk/payload_schemas/agent.py +1 -0
  98. glaip_sdk/payload_schemas/guardrails.py +34 -0
  99. glaip_sdk/registry/tool.py +273 -59
  100. glaip_sdk/runner/__init__.py +20 -3
  101. glaip_sdk/runner/deps.py +5 -8
  102. glaip_sdk/runner/langgraph.py +318 -42
  103. glaip_sdk/runner/logging_config.py +77 -0
  104. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  105. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  106. glaip_sdk/schedules/__init__.py +22 -0
  107. glaip_sdk/schedules/base.py +291 -0
  108. glaip_sdk/tools/base.py +67 -14
  109. glaip_sdk/utils/__init__.py +1 -0
  110. glaip_sdk/utils/bundler.py +138 -2
  111. glaip_sdk/utils/import_resolver.py +43 -11
  112. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  113. glaip_sdk/utils/runtime_config.py +15 -12
  114. glaip_sdk/utils/sync.py +31 -11
  115. glaip_sdk/utils/tool_detection.py +274 -6
  116. glaip_sdk/utils/tool_storage_provider.py +140 -0
  117. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/METADATA +49 -37
  118. glaip_sdk-0.7.12.dist-info/RECORD +219 -0
  119. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/WHEEL +2 -1
  120. glaip_sdk-0.7.12.dist-info/entry_points.txt +2 -0
  121. glaip_sdk-0.7.12.dist-info/top_level.txt +1 -0
  122. glaip_sdk/cli/commands/agents.py +0 -1509
  123. glaip_sdk/cli/commands/mcps.py +0 -1356
  124. glaip_sdk/cli/commands/tools.py +0 -576
  125. glaip_sdk/cli/utils.py +0 -263
  126. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  127. glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
glaip_sdk/__init__.py CHANGED
@@ -4,12 +4,49 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ from typing import TYPE_CHECKING, Any
11
+
7
12
  from glaip_sdk._version import __version__
8
- from glaip_sdk.client import Client
9
- from glaip_sdk.exceptions import AIPError
10
- from glaip_sdk.agents import Agent
11
- from glaip_sdk.tools import Tool
12
- from glaip_sdk.mcps import MCP
13
13
 
14
+ if TYPE_CHECKING: # pragma: no cover - import only for type checking
15
+ from glaip_sdk.agents import Agent
16
+ from glaip_sdk.client import Client
17
+ from glaip_sdk.exceptions import AIPError
18
+ from glaip_sdk.mcps import MCP
19
+ from glaip_sdk.tools import Tool
14
20
 
15
21
  __all__ = ["Client", "Agent", "Tool", "MCP", "AIPError", "__version__"]
22
+
23
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
24
+ "Client": ("glaip_sdk.client", "Client"),
25
+ "Agent": ("glaip_sdk.agents", "Agent"),
26
+ "Tool": ("glaip_sdk.tools", "Tool"),
27
+ "MCP": ("glaip_sdk.mcps", "MCP"),
28
+ "AIPError": ("glaip_sdk.exceptions", "AIPError"),
29
+ }
30
+
31
+
32
+ def __getattr__(name: str) -> Any:
33
+ """Lazy attribute access for public SDK symbols to defer heavy imports."""
34
+ if name == "__version__":
35
+ # Import __version__ when accessed via __getattr__
36
+ # This ensures coverage even if __version__ was removed from __dict__ for testing
37
+ from glaip_sdk._version import __version__ as version # noqa: PLC0415
38
+
39
+ globals()["__version__"] = version
40
+ return version
41
+ if name in _LAZY_IMPORTS:
42
+ module_path, attr_name = _LAZY_IMPORTS[name]
43
+ module = importlib.import_module(module_path)
44
+ attr = getattr(module, attr_name)
45
+ globals()[name] = attr
46
+ return attr
47
+ raise AttributeError(f"module 'glaip_sdk' has no attribute {name!r}")
48
+
49
+
50
+ def __dir__() -> list[str]:
51
+ """Return module attributes for dir()."""
52
+ return sorted(__all__)
glaip_sdk/agents/base.py CHANGED
@@ -50,16 +50,11 @@ from pathlib import Path
50
50
  from typing import TYPE_CHECKING, Any
51
51
 
52
52
  from glaip_sdk.registry import get_agent_registry, get_mcp_registry, get_tool_registry
53
- from glaip_sdk.runner import get_default_runner
54
- from glaip_sdk.runner.deps import (
55
- check_local_runtime_available,
56
- get_local_runtime_missing_message,
57
- )
58
- from glaip_sdk.utils.discovery import find_agent
59
53
  from glaip_sdk.utils.resource_refs import is_uuid
60
- from glaip_sdk.utils.runtime_config import normalize_runtime_config_keys
61
54
 
62
55
  if TYPE_CHECKING:
56
+ from glaip_sdk.client.schedules import AgentScheduleManager
57
+ from glaip_sdk.guardrails import GuardrailManager
63
58
  from glaip_sdk.models import AgentResponse
64
59
  from glaip_sdk.registry import AgentRegistry, MCPRegistry, ToolRegistry
65
60
 
@@ -136,6 +131,7 @@ class Agent:
136
131
  agents: list | None = None,
137
132
  mcps: list | None = None,
138
133
  model: str | None = _UNSET, # type: ignore[assignment]
134
+ guardrail: GuardrailManager | None = None,
139
135
  _client: Any = None,
140
136
  **kwargs: Any,
141
137
  ) -> None:
@@ -153,7 +149,9 @@ class Agent:
153
149
  agents: List of sub-agents (Agent classes, instances, or strings).
154
150
  mcps: List of MCPs.
155
151
  model: Model identifier.
152
+ guardrail: The guardrail manager for content safety.
156
153
  _client: Internal client reference (set automatically).
154
+
157
155
  **kwargs: Additional configuration parameters:
158
156
  - timeout: Execution timeout in seconds.
159
157
  - metadata: Optional metadata dictionary.
@@ -179,6 +177,7 @@ class Agent:
179
177
  self._agents = agents
180
178
  self._mcps = mcps
181
179
  self._model = model
180
+ self._guardrail = guardrail
182
181
  self._language_model_id: str | None = None
183
182
  # Extract parameters from kwargs with _UNSET defaults
184
183
  self._timeout = kwargs.pop("timeout", Agent._UNSET) # type: ignore[assignment]
@@ -458,6 +457,11 @@ class Agent:
458
457
  return self._mcp_configs
459
458
  return None
460
459
 
460
+ @property
461
+ def guardrail(self) -> GuardrailManager | None:
462
+ """The guardrail manager for content safety."""
463
+ return self._guardrail
464
+
461
465
  @property
462
466
  def a2a_profile(self) -> dict[str, Any] | None:
463
467
  """A2A (Agent-to-Agent) profile configuration.
@@ -518,6 +522,8 @@ class Agent:
518
522
  from glaip_sdk.utils.client import get_client # noqa: PLC0415
519
523
 
520
524
  client = get_client()
525
+ from glaip_sdk.utils.discovery import find_agent # noqa: PLC0415
526
+
521
527
  response = self._create_or_update_agent(config, client, find_agent)
522
528
 
523
529
  # Update self with deployed info
@@ -551,11 +557,20 @@ class Agent:
551
557
 
552
558
  # Handle agent_config with timeout
553
559
  # The timeout property is a convenience that maps to agent_config.execution_timeout
554
- agent_config = dict(self.agent_config) if self.agent_config else {}
560
+ raw_config = self.agent_config if self.agent_config is not self._UNSET else {}
561
+ agent_config = dict(raw_config) if raw_config else {}
562
+
555
563
  if self.timeout and "execution_timeout" not in agent_config:
556
564
  agent_config["execution_timeout"] = self.timeout
557
- if agent_config:
558
- config["agent_config"] = agent_config
565
+
566
+ if self.guardrail:
567
+ from glaip_sdk.guardrails.serializer import ( # noqa: PLC0415
568
+ serialize_guardrail_manager,
569
+ )
570
+
571
+ agent_config["guardrails"] = serialize_guardrail_manager(self.guardrail)
572
+
573
+ config["agent_config"] = agent_config
559
574
 
560
575
  # Handle tool_configs - resolve tool names/classes to IDs
561
576
  if self.tool_configs:
@@ -587,11 +602,20 @@ class Agent:
587
602
 
588
603
  Returns:
589
604
  List of resolved MCP IDs for the API payload.
605
+
606
+ Raises:
607
+ ValueError: If an MCP fails to resolve to a valid ID.
590
608
  """
591
609
  if not self.mcps:
592
610
  return []
593
611
 
594
- return [registry.resolve(mcp_ref).id for mcp_ref in self.mcps]
612
+ resolved_ids: list[str] = []
613
+ for mcp_ref in self.mcps:
614
+ mcp = registry.resolve(mcp_ref)
615
+ if not mcp.id:
616
+ raise ValueError(f"Failed to resolve ID for MCP: {mcp_ref}")
617
+ resolved_ids.append(mcp.id)
618
+ return resolved_ids
595
619
 
596
620
  def _resolve_tools(self, registry: ToolRegistry) -> list[str]:
597
621
  """Resolve tool references to IDs using ToolRegistry.
@@ -605,12 +629,20 @@ class Agent:
605
629
 
606
630
  Returns:
607
631
  List of resolved tool IDs for the API payload.
632
+
633
+ Raises:
634
+ ValueError: If a tool fails to resolve to a valid ID.
608
635
  """
609
636
  if not self.tools:
610
637
  return []
611
638
 
612
- # Resolve each tool reference to a Tool object, extract ID
613
- return [registry.resolve(tool_ref).id for tool_ref in self.tools]
639
+ resolved_ids: list[str] = []
640
+ for tool_ref in self.tools:
641
+ tool = registry.resolve(tool_ref)
642
+ if not tool.id:
643
+ raise ValueError(f"Failed to resolve ID for tool: {tool_ref}")
644
+ resolved_ids.append(tool.id)
645
+ return resolved_ids
614
646
 
615
647
  def _resolve_tool_configs(self, registry: ToolRegistry) -> dict[str, Any]:
616
648
  """Resolve tool_configs keys from tool names/classes to tool IDs.
@@ -653,6 +685,8 @@ class Agent:
653
685
  try:
654
686
  # Resolve key (tool name/class) to Tool object, get ID
655
687
  tool = registry.resolve(key)
688
+ if not tool.id:
689
+ raise ValueError(f"Resolved tool has no ID: {key}")
656
690
  resolved[tool.id] = config
657
691
  except (ValueError, KeyError) as e:
658
692
  raise ValueError(f"Failed to resolve tool config key: {key}") from e
@@ -688,6 +722,8 @@ class Agent:
688
722
  resolved_id = key
689
723
  else:
690
724
  mcp = registry.resolve(key)
725
+ if not mcp.id:
726
+ raise ValueError(f"Resolved MCP has no ID: {key}")
691
727
  resolved_id = mcp.id
692
728
 
693
729
  if resolved_id in resolved:
@@ -703,7 +739,7 @@ class Agent:
703
739
 
704
740
  return resolved
705
741
 
706
- def _resolve_agents(self, registry: AgentRegistry) -> list:
742
+ def _resolve_agents(self, registry: AgentRegistry) -> list[str]:
707
743
  """Resolve sub-agent references using AgentRegistry.
708
744
 
709
745
  Uses the global AgentRegistry to cache Agent objects across deployments.
@@ -715,12 +751,20 @@ class Agent:
715
751
 
716
752
  Returns:
717
753
  List of resolved agent IDs for the API payload.
754
+
755
+ Raises:
756
+ ValueError: If an agent fails to resolve to a valid ID.
718
757
  """
719
758
  if not self.agents:
720
759
  return []
721
760
 
722
- # Resolve each agent reference to a deployed Agent, extract ID
723
- return [registry.resolve(agent_ref).id for agent_ref in self.agents]
761
+ resolved_ids: list[str] = []
762
+ for agent_ref in self.agents:
763
+ agent = registry.resolve(agent_ref)
764
+ if not agent.id:
765
+ raise ValueError(f"Failed to resolve ID for agent: {agent_ref}")
766
+ resolved_ids.append(agent.id)
767
+ return resolved_ids
724
768
 
725
769
  def _create_or_update_agent(
726
770
  self,
@@ -811,6 +855,8 @@ class Agent:
811
855
  Returns:
812
856
  Dictionary containing Agent attributes.
813
857
  """
858
+ config = self._build_config(get_tool_registry(), get_mcp_registry())
859
+
814
860
  data = {
815
861
  "id": self._id,
816
862
  "name": self.name,
@@ -824,10 +870,11 @@ class Agent:
824
870
  "mcps": self.mcps,
825
871
  "timeout": self.timeout,
826
872
  "metadata": self.metadata,
827
- "agent_config": self.agent_config,
873
+ "agent_config": config.get("agent_config"),
828
874
  "tool_configs": self.tool_configs,
829
875
  "mcp_configs": self.mcp_configs,
830
876
  "a2a_profile": self.a2a_profile,
877
+ "guardrail": self.guardrail,
831
878
  "created_at": self._created_at,
832
879
  "updated_at": self._updated_at,
833
880
  }
@@ -847,6 +894,36 @@ class Agent:
847
894
  self._client = client
848
895
  return self
849
896
 
897
+ @property
898
+ def schedule(self) -> AgentScheduleManager:
899
+ """Get the schedule manager for this agent.
900
+
901
+ Provides a convenient interface for managing schedules scoped to this agent.
902
+
903
+ Returns:
904
+ AgentScheduleManager for schedule operations
905
+
906
+ Raises:
907
+ ValueError: If agent is not deployed
908
+ RuntimeError: If agent is not bound to a client
909
+
910
+ Example:
911
+ >>> agent = client.get_agent_by_id("agent-id")
912
+ >>> schedules = agent.schedule.list()
913
+ >>> new_schedule = agent.schedule.create(
914
+ ... input="Daily task",
915
+ ... schedule="0 9 * * 1-5"
916
+ ... )
917
+ """
918
+ if not self.id:
919
+ raise ValueError(_AGENT_NOT_DEPLOYED_MSG)
920
+ if not self._client:
921
+ raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
922
+
923
+ from glaip_sdk.client.schedules import AgentScheduleManager # noqa: PLC0415
924
+
925
+ return AgentScheduleManager(self, self._client.schedules)
926
+
850
927
  def _prepare_run_kwargs(
851
928
  self,
852
929
  message: str,
@@ -869,7 +946,7 @@ class Agent:
869
946
  ValueError: If the agent hasn't been deployed yet.
870
947
  RuntimeError: If client is not available.
871
948
  """
872
- if not self.id:
949
+ if not self.id: # pragma: no cover - defensive: called only when self.id is truthy
873
950
  raise ValueError(_AGENT_NOT_DEPLOYED_MSG)
874
951
  if not self._client:
875
952
  raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
@@ -883,6 +960,10 @@ class Agent:
883
960
  }
884
961
 
885
962
  if runtime_config is not None:
963
+ from glaip_sdk.utils.runtime_config import ( # noqa: PLC0415
964
+ normalize_runtime_config_keys,
965
+ )
966
+
886
967
  call_kwargs["runtime_config"] = normalize_runtime_config_keys(
887
968
  runtime_config,
888
969
  tool_registry=get_tool_registry(),
@@ -893,10 +974,68 @@ class Agent:
893
974
  call_kwargs.update(kwargs)
894
975
  return agent_client, call_kwargs
895
976
 
977
+ def _get_local_runner_or_raise(self) -> Any:
978
+ """Get the local runner if available, otherwise raise ValueError.
979
+
980
+ Returns:
981
+ The default local runner instance.
982
+
983
+ Raises:
984
+ ValueError: If local runtime is not available.
985
+ """
986
+ from glaip_sdk.runner import get_default_runner # noqa: PLC0415
987
+ from glaip_sdk.runner.deps import ( # noqa: PLC0415
988
+ check_local_runtime_available,
989
+ get_local_runtime_missing_message,
990
+ )
991
+
992
+ if check_local_runtime_available():
993
+ return get_default_runner()
994
+
995
+ # If agent is not deployed, it *must* use local runtime
996
+ if not self.id:
997
+ raise ValueError(f"{_AGENT_NOT_DEPLOYED_MSG}\n\n{get_local_runtime_missing_message()}")
998
+
999
+ # If agent IS deployed but local execution was forced (local=True)
1000
+ raise ValueError(
1001
+ f"Local execution override was requested, but local runtime is missing.\n\n"
1002
+ f"{get_local_runtime_missing_message()}"
1003
+ )
1004
+
1005
+ def _prepare_local_runner_kwargs(
1006
+ self,
1007
+ message: str,
1008
+ verbose: bool,
1009
+ runtime_config: dict[str, Any] | None,
1010
+ chat_history: list[dict[str, str]] | None,
1011
+ **kwargs: Any,
1012
+ ) -> dict[str, Any]:
1013
+ """Prepare kwargs for local runner execution.
1014
+
1015
+ Args:
1016
+ message: The message to send to the agent.
1017
+ verbose: If True, print streaming output to console.
1018
+ runtime_config: Optional runtime configuration.
1019
+ chat_history: Optional list of prior conversation messages.
1020
+ **kwargs: Additional arguments.
1021
+
1022
+ Returns:
1023
+ Dictionary of prepared kwargs for runner.run() or runner.arun().
1024
+ """
1025
+ return {
1026
+ "agent": self,
1027
+ "message": message,
1028
+ "verbose": verbose,
1029
+ "runtime_config": runtime_config,
1030
+ "chat_history": chat_history,
1031
+ **kwargs,
1032
+ }
1033
+
896
1034
  def run(
897
1035
  self,
898
1036
  message: str,
899
1037
  verbose: bool = False,
1038
+ local: bool = False,
900
1039
  runtime_config: dict[str, Any] | None = None,
901
1040
  chat_history: list[dict[str, str]] | None = None,
902
1041
  **kwargs: Any,
@@ -909,9 +1048,13 @@ class Agent:
909
1048
  - **Local**: When the agent is not deployed and glaip-sdk[local] is installed,
910
1049
  execution happens locally via aip-agents (no server required).
911
1050
 
1051
+ You can force local execution for a deployed agent by passing `local=True`.
1052
+
912
1053
  Args:
913
1054
  message: The message to send to the agent.
914
1055
  verbose: If True, print streaming output to console. Defaults to False.
1056
+ local: If True, force local execution even if the agent is deployed.
1057
+ Defaults to False.
915
1058
  runtime_config: Optional runtime configuration for tools, MCPs, and agents.
916
1059
  Keys can be SDK objects, UUIDs, or names. Example:
917
1060
  {
@@ -933,42 +1076,47 @@ class Agent:
933
1076
  RuntimeError: If server-backed execution fails due to client issues.
934
1077
  """
935
1078
  # Backend routing: deployed agents use server, undeployed use local (if available)
936
- if self.id:
1079
+ if self.id and not local:
937
1080
  # Server-backed execution path (agent is deployed)
938
1081
  agent_client, call_kwargs = self._prepare_run_kwargs(
939
- message, verbose, runtime_config or kwargs.get("runtime_config"), **kwargs
1082
+ message,
1083
+ verbose,
1084
+ runtime_config or kwargs.get("runtime_config"),
1085
+ **kwargs,
940
1086
  )
941
1087
  if chat_history is not None:
942
1088
  call_kwargs["chat_history"] = chat_history
943
1089
  return agent_client.run_agent(**call_kwargs)
944
1090
 
945
- # Local execution path (agent is not deployed)
946
- if check_local_runtime_available():
947
- runner = get_default_runner()
948
- return runner.run(
949
- agent=self,
950
- message=message,
951
- verbose=verbose,
952
- runtime_config=runtime_config,
953
- chat_history=chat_history,
954
- **kwargs,
955
- )
956
-
957
- # Neither deployed nor local runtime available - provide actionable error
958
- raise ValueError(f"{_AGENT_NOT_DEPLOYED_MSG}\n\n{get_local_runtime_missing_message()}")
1091
+ # Local execution path (agent is not deployed OR local=True)
1092
+ runner = self._get_local_runner_or_raise()
1093
+ local_kwargs = self._prepare_local_runner_kwargs(message, verbose, runtime_config, chat_history, **kwargs)
1094
+ return runner.run(**local_kwargs)
959
1095
 
960
1096
  async def arun(
961
1097
  self,
962
1098
  message: str,
963
1099
  verbose: bool = False,
1100
+ local: bool = False,
964
1101
  runtime_config: dict[str, Any] | None = None,
1102
+ chat_history: list[dict[str, str]] | None = None,
965
1103
  **kwargs: Any,
966
1104
  ) -> AsyncGenerator[dict, None]:
967
1105
  """Run the agent asynchronously with streaming output.
968
1106
 
1107
+ Supports two execution modes:
1108
+ - **Server-backed**: When the agent is deployed (has an ID), execution
1109
+ happens via the AIP backend server with streaming.
1110
+ - **Local**: When the agent is not deployed and glaip-sdk[local] is installed,
1111
+ execution happens locally via aip-agents (no server required).
1112
+
1113
+ You can force local execution for a deployed agent by passing `local=True`.
1114
+
969
1115
  Args:
970
1116
  message: The message to send to the agent.
971
- verbose: If True, print streaming output to console.
1117
+ verbose: If True, print streaming output to console. Defaults to False.
1118
+ local: If True, force local execution even if the agent is deployed.
1119
+ Defaults to False.
972
1120
  runtime_config: Optional runtime configuration for tools, MCPs, and agents.
973
1121
  Keys can be SDK objects, UUIDs, or names. Example:
974
1122
  {
@@ -976,20 +1124,47 @@ class Agent:
976
1124
  "mcp_configs": {"mcp-id": {"setting": "on"}},
977
1125
  "agent_config": {"planning": True},
978
1126
  }
1127
+ Defaults to None.
1128
+ chat_history: Optional list of prior conversation messages for context.
1129
+ Each message is a dict with "role" and "content" keys.
1130
+ Defaults to None.
979
1131
  **kwargs: Additional arguments to pass to the run API.
980
1132
 
981
1133
  Yields:
982
1134
  Streaming response chunks from the agent.
983
1135
 
984
1136
  Raises:
985
- ValueError: If the agent hasn't been deployed yet.
986
- RuntimeError: If client is not available.
1137
+ ValueError: If the agent is not deployed and local runtime is not available.
1138
+ RuntimeError: If server-backed execution fails due to client issues.
987
1139
  """
988
- agent_client, call_kwargs = self._prepare_run_kwargs(
989
- message, verbose, runtime_config or kwargs.get("runtime_config"), **kwargs
990
- )
991
- async for chunk in agent_client.arun_agent(**call_kwargs):
992
- yield chunk
1140
+ # Backend routing: deployed agents use server, undeployed use local (if available)
1141
+ if self.id and not local:
1142
+ # Server-backed execution path (agent is deployed)
1143
+ agent_client, call_kwargs = self._prepare_run_kwargs(
1144
+ message,
1145
+ verbose,
1146
+ runtime_config or kwargs.get("runtime_config"),
1147
+ **kwargs,
1148
+ )
1149
+ if chat_history is not None:
1150
+ call_kwargs["chat_history"] = chat_history
1151
+
1152
+ async for chunk in agent_client.arun_agent(**call_kwargs):
1153
+ yield chunk
1154
+ return
1155
+
1156
+ # Local execution path (agent is not deployed OR local=True)
1157
+ runner = self._get_local_runner_or_raise()
1158
+ local_kwargs = self._prepare_local_runner_kwargs(message, verbose, runtime_config, chat_history, **kwargs)
1159
+ result = await runner.arun(**local_kwargs)
1160
+ # Yield a final_response event for consistency with server-backed execution
1161
+ # Include event_type for A2A event shape parity
1162
+ yield {
1163
+ "event_type": "final_response",
1164
+ "metadata": {"kind": "final_response"},
1165
+ "content": result,
1166
+ "is_final": True,
1167
+ }
993
1168
 
994
1169
  def update(self, **kwargs: Any) -> Agent:
995
1170
  """Update the deployed agent with new configuration.
glaip_sdk/branding.py CHANGED
@@ -17,6 +17,7 @@ import platform
17
17
  import sys
18
18
 
19
19
  from rich.console import Console
20
+ from rich.text import Text
20
21
 
21
22
  from glaip_sdk._version import __version__ as SDK_VERSION
22
23
  from glaip_sdk.rich_components import AIPPanel
@@ -110,9 +111,13 @@ GDP Labs AI Agents Package
110
111
  return SDK_VERSION
111
112
 
112
113
  @staticmethod
113
- def _make_console() -> Console:
114
+ def _make_console(force_terminal: bool | None = None, *, soft_wrap: bool = True) -> Console:
114
115
  """Create a Rich Console instance respecting NO_COLOR environment variables.
115
116
 
117
+ Args:
118
+ force_terminal: Override terminal detection when True/False.
119
+ soft_wrap: Whether to enable soft wrapping in the console.
120
+
116
121
  Returns:
117
122
  Console instance with color system configured based on environment.
118
123
  """
@@ -124,7 +129,12 @@ GDP Labs AI Agents Package
124
129
  else:
125
130
  color_system = "auto"
126
131
  no_color = False
127
- return Console(color_system=color_system, no_color=no_color, soft_wrap=True)
132
+ return Console(
133
+ color_system=color_system,
134
+ no_color=no_color,
135
+ soft_wrap=soft_wrap,
136
+ force_terminal=force_terminal,
137
+ )
128
138
 
129
139
  # ---- public API -----------------------------------------------------------
130
140
  def get_welcome_banner(self) -> str:
@@ -209,3 +219,104 @@ GDP Labs AI Agents Package
209
219
  AIPBranding instance
210
220
  """
211
221
  return cls(version=sdk_version, package_name=package_name)
222
+
223
+
224
+ class LogoAnimator:
225
+ """Animated logo with pulse effect for CLI startup.
226
+
227
+ Provides a "Knight Rider" style light pulse animation that sweeps across
228
+ the GL AIP logo during initialization tasks. Respects NO_COLOR and non-TTY
229
+ environments with graceful degradation.
230
+ """
231
+
232
+ # Animation colors from GDP Labs brand palette
233
+ BASE_BLUE = SECONDARY_MEDIUM # "#005CB8" - Medium Blue
234
+ HIGHLIGHT = SECONDARY_LIGHT # "#40B4E5" - Light Blue
235
+ WHITE = "#FFFFFF" # Bright white center
236
+
237
+ def __init__(self, console: Console | None = None) -> None:
238
+ """Initialize LogoAnimator.
239
+
240
+ Args:
241
+ console: Optional console instance. If None, creates a default console.
242
+ """
243
+ self.console = console or AIPBranding._make_console()
244
+ self.logo = AIPBranding.AIP_LOGO
245
+ self.lines = self.logo.split("\n")
246
+ self.max_width = max(len(line) for line in self.lines) if self.lines else 0
247
+
248
+ def generate_frame(self, step: int, status_text: str = "") -> Text:
249
+ """Generate a single animation frame with logo pulse and status.
250
+
251
+ Args:
252
+ step: Current animation step (position of the pulse).
253
+ status_text: Optional status text to display below the logo.
254
+
255
+ Returns:
256
+ Text object with styled logo and status.
257
+ """
258
+ text = Text()
259
+
260
+ for line in self.lines:
261
+ for x, char in enumerate(line):
262
+ distance = abs(x - step)
263
+
264
+ if distance == 0:
265
+ style = f"bold {self.WHITE}" # Bright white center
266
+ elif distance <= 3:
267
+ style = f"bold {self.HIGHLIGHT}" # Light blue glow
268
+ else:
269
+ style = self.BASE_BLUE # Base blue
270
+
271
+ text.append(char, style=style)
272
+ text.append("\n")
273
+
274
+ # Add status area below the logo
275
+ if status_text:
276
+ text.append(f"\n{status_text}\n")
277
+
278
+ return text
279
+
280
+ def should_animate(self) -> bool:
281
+ """Check if animation should be used.
282
+
283
+ Returns:
284
+ True if animation should be used (interactive TTY with colors),
285
+ False otherwise (NO_COLOR set or non-TTY).
286
+ """
287
+ # Check for NO_COLOR environment variables
288
+ no_color = os.getenv("NO_COLOR") is not None or os.getenv("AIP_NO_COLOR") is not None
289
+ if no_color:
290
+ return False
291
+
292
+ # Check if console is a TTY
293
+ if not self.console.is_terminal:
294
+ return False
295
+
296
+ # Check if console explicitly disables colors
297
+ if self.console.no_color:
298
+ return False
299
+
300
+ # If we get here, we have a TTY without NO_COLOR set
301
+ # Rich will handle color detection, so we can animate
302
+ return True
303
+
304
+ def display_static_logo(self, status_text: str = "") -> None:
305
+ """Display static logo without animation (for non-TTY or NO_COLOR).
306
+
307
+ Args:
308
+ status_text: Optional status text to display below the logo.
309
+ """
310
+ self.console.print(self.static_frame(status_text))
311
+
312
+ def static_frame(self, status_text: str = "") -> Text:
313
+ """Return a static logo frame for use in non-animated renders.
314
+
315
+ Args:
316
+ status_text: Optional status text to display below the logo.
317
+ """
318
+ logo_text = Text(self.logo, style=self.BASE_BLUE)
319
+ if status_text:
320
+ logo_text.append("\n")
321
+ logo_text.append(status_text)
322
+ return logo_text
@@ -523,6 +523,21 @@ class AccountStore:
523
523
 
524
524
  self._save_config(config)
525
525
 
526
+ def save_config_updates(self, config: dict[str, Any]) -> None:
527
+ """Save config updates, preserving all existing keys.
528
+
529
+ This method allows external code to update arbitrary config keys
530
+ (e.g., TUI preferences) while preserving the full config structure.
531
+
532
+ Args:
533
+ config: Complete configuration dictionary to save. This should
534
+ include all keys that should be preserved, not just updates.
535
+
536
+ Raises:
537
+ AccountStoreError: If config file cannot be written.
538
+ """
539
+ self._save_config(config)
540
+
526
541
 
527
542
  # Global instance for convenience
528
543
  _account_store = AccountStore()