glaip-sdk 0.0.15__py3-none-any.whl → 0.0.16__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 (40) hide show
  1. glaip_sdk/branding.py +27 -1
  2. glaip_sdk/cli/commands/agents.py +26 -17
  3. glaip_sdk/cli/commands/configure.py +39 -50
  4. glaip_sdk/cli/commands/mcps.py +1 -3
  5. glaip_sdk/cli/config.py +42 -0
  6. glaip_sdk/cli/display.py +92 -26
  7. glaip_sdk/cli/main.py +141 -124
  8. glaip_sdk/cli/mcp_validators.py +2 -2
  9. glaip_sdk/cli/pager.py +3 -2
  10. glaip_sdk/cli/parsers/json_input.py +2 -2
  11. glaip_sdk/cli/resolution.py +12 -10
  12. glaip_sdk/cli/slash/agent_session.py +7 -0
  13. glaip_sdk/cli/slash/prompt.py +21 -2
  14. glaip_sdk/cli/slash/session.py +15 -21
  15. glaip_sdk/cli/update_notifier.py +8 -2
  16. glaip_sdk/cli/utils.py +110 -53
  17. glaip_sdk/client/_agent_payloads.py +504 -0
  18. glaip_sdk/client/agents.py +194 -551
  19. glaip_sdk/client/base.py +92 -20
  20. glaip_sdk/client/main.py +6 -0
  21. glaip_sdk/client/run_rendering.py +275 -0
  22. glaip_sdk/config/constants.py +3 -0
  23. glaip_sdk/exceptions.py +15 -0
  24. glaip_sdk/models.py +5 -0
  25. glaip_sdk/payload_schemas/__init__.py +19 -0
  26. glaip_sdk/payload_schemas/agent.py +87 -0
  27. glaip_sdk/rich_components.py +12 -0
  28. glaip_sdk/utils/client_utils.py +12 -0
  29. glaip_sdk/utils/import_export.py +2 -2
  30. glaip_sdk/utils/rendering/formatting.py +5 -0
  31. glaip_sdk/utils/rendering/models.py +22 -0
  32. glaip_sdk/utils/rendering/renderer/base.py +9 -1
  33. glaip_sdk/utils/rendering/renderer/panels.py +0 -1
  34. glaip_sdk/utils/rendering/steps.py +59 -0
  35. glaip_sdk/utils/serialization.py +24 -3
  36. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/METADATA +1 -1
  37. glaip_sdk-0.0.16.dist-info/RECORD +72 -0
  38. glaip_sdk-0.0.15.dist-info/RECORD +0 -67
  39. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/WHEEL +0 -0
  40. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/entry_points.txt +0 -0
@@ -5,20 +5,26 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
- import io
9
8
  import json
10
9
  import logging
11
- from collections.abc import AsyncGenerator
12
- from time import monotonic
10
+ from collections.abc import AsyncGenerator, Mapping
13
11
  from typing import Any, BinaryIO
14
12
 
15
13
  import httpx
16
- from rich.console import Console as _Console
17
14
 
15
+ from glaip_sdk.client._agent_payloads import (
16
+ AgentCreateRequest,
17
+ AgentListParams,
18
+ AgentListResult,
19
+ AgentUpdateRequest,
20
+ )
18
21
  from glaip_sdk.client.base import BaseClient
22
+ from glaip_sdk.client.run_rendering import (
23
+ AgentRunRenderingManager,
24
+ compute_timeout_seconds,
25
+ )
19
26
  from glaip_sdk.config.constants import (
20
27
  DEFAULT_AGENT_FRAMEWORK,
21
- DEFAULT_AGENT_PROVIDER,
22
28
  DEFAULT_AGENT_RUN_TIMEOUT,
23
29
  DEFAULT_AGENT_TYPE,
24
30
  DEFAULT_AGENT_VERSION,
@@ -29,14 +35,10 @@ from glaip_sdk.models import Agent
29
35
  from glaip_sdk.utils.client_utils import (
30
36
  aiter_sse_events,
31
37
  create_model_instances,
32
- extract_ids,
33
38
  find_by_name,
34
- iter_sse_events,
35
39
  prepare_multipart_data,
36
40
  )
37
- from glaip_sdk.utils.rendering.models import RunStats
38
41
  from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
39
- from glaip_sdk.utils.rendering.renderer.config import RendererConfig
40
42
  from glaip_sdk.utils.validation import validate_agent_instruction
41
43
 
42
44
  # API endpoints
@@ -65,44 +67,51 @@ class AgentClient(BaseClient):
65
67
  **kwargs: Additional arguments for standalone initialization
66
68
  """
67
69
  super().__init__(parent_client=parent_client, **kwargs)
70
+ self._renderer_manager = AgentRunRenderingManager(logger)
68
71
 
69
72
  def list_agents(
70
73
  self,
71
- agent_type: str | None = None,
72
- framework: str | None = None,
73
- name: str | None = None,
74
- version: str | None = None,
75
- sync_langflow_agents: bool = False,
76
- ) -> list[Agent]:
77
- """List agents with optional filtering.
74
+ query: AgentListParams | None = None,
75
+ **kwargs: Any,
76
+ ) -> AgentListResult:
77
+ """List agents with optional filtering and pagination support.
78
78
 
79
79
  Args:
80
- agent_type: Filter by agent type (config, code, a2a)
81
- framework: Filter by framework (langchain, langgraph, google_adk)
82
- name: Filter by partial name match (case-insensitive)
83
- version: Filter by exact version match
84
- sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
85
-
86
- Returns:
87
- List of agents matching the filters
80
+ query: Query parameters for filtering agents. If None, uses kwargs to create query.
81
+ **kwargs: Individual filter parameters for backward compatibility.
88
82
  """
89
- params = {}
90
- if agent_type is not None:
91
- params["agent_type"] = agent_type
92
- if framework is not None:
93
- params["framework"] = framework
94
- if name is not None:
95
- params["name"] = name
96
- if version is not None:
97
- params["version"] = version
98
- if sync_langflow_agents:
99
- params["sync_langflow_agents"] = "true"
100
-
101
- if params:
102
- data = self._request("GET", AGENTS_ENDPOINT, params=params)
103
- else:
104
- data = self._request("GET", AGENTS_ENDPOINT)
105
- return create_model_instances(data, Agent, self)
83
+ if query is not None and kwargs:
84
+ # Both query object and individual parameters provided
85
+ raise ValueError(
86
+ "Provide either `query` or individual filter arguments, not both."
87
+ )
88
+
89
+ if query is None:
90
+ # Create query from individual parameters for backward compatibility
91
+ query = AgentListParams(**kwargs)
92
+
93
+ params = query.to_query_params()
94
+ envelope = self._request_with_envelope(
95
+ "GET",
96
+ AGENTS_ENDPOINT,
97
+ params=params if params else None,
98
+ )
99
+
100
+ if not isinstance(envelope, dict):
101
+ envelope = {"data": envelope}
102
+
103
+ data_payload = envelope.get("data") or []
104
+ items = create_model_instances(data_payload, Agent, self)
105
+
106
+ return AgentListResult(
107
+ items=items,
108
+ total=envelope.get("total"),
109
+ page=envelope.get("page"),
110
+ limit=envelope.get("limit"),
111
+ has_next=envelope.get("has_next"),
112
+ has_prev=envelope.get("has_prev"),
113
+ message=envelope.get("message"),
114
+ )
106
115
 
107
116
  def sync_langflow_agents(
108
117
  self,
@@ -151,292 +160,65 @@ class AgentClient(BaseClient):
151
160
 
152
161
  def find_agents(self, name: str | None = None) -> list[Agent]:
153
162
  """Find agents by name."""
154
- params = {}
155
- if name:
156
- params["name"] = name
157
-
158
- data = self._request("GET", AGENTS_ENDPOINT, params=params)
159
- agents = create_model_instances(data, Agent, self)
163
+ result = self.list_agents(name=name)
164
+ agents = list(result)
160
165
  if name is None:
161
166
  return agents
162
167
  return find_by_name(agents, name, case_sensitive=False)
163
168
 
164
- def _build_create_payload(
165
- self,
166
- name: str,
167
- instruction: str,
168
- model: str = DEFAULT_MODEL,
169
- tools: list[str | Any] | None = None,
170
- agents: list[str | Any] | None = None,
171
- timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
172
- **kwargs: Any,
173
- ) -> dict[str, Any]:
174
- """Build payload for agent creation with proper LM selection and metadata handling.
175
-
176
- CENTRALIZED PAYLOAD BUILDING LOGIC:
177
- - LM exclusivity: Uses language_model_id if provided, otherwise provider/model_name
178
- - Always includes required backend metadata
179
- - Preserves mem0 keys in agent_config
180
- - Handles tool/agent ID extraction from objects
181
-
182
- Args:
183
- name: Agent name
184
- instruction: Agent instruction
185
- model: Language model name (used when language_model_id not provided)
186
- tools: List of tools to attach
187
- agents: List of sub-agents to attach
188
- timeout: Agent execution timeout
189
- **kwargs: Additional parameters (language_model_id, agent_config, etc.)
190
-
191
- Returns:
192
- Complete payload dictionary for agent creation
193
- """
194
- # Prepare the creation payload with required fields
195
- payload: dict[str, Any] = {
196
- "name": name.strip(),
197
- "instruction": instruction.strip(),
198
- "type": DEFAULT_AGENT_TYPE,
199
- "framework": DEFAULT_AGENT_FRAMEWORK,
200
- "version": DEFAULT_AGENT_VERSION,
201
- }
202
-
203
- # Language model selection with exclusivity:
204
- # Priority: language_model_id (if provided) > provider/model_name (fallback)
205
- if kwargs.get("language_model_id"):
206
- # Use language_model_id - defer to kwargs update below
207
- pass
208
- else:
209
- # Use provider/model_name fallback
210
- payload["provider"] = DEFAULT_AGENT_PROVIDER
211
- payload["model_name"] = model or DEFAULT_MODEL
212
-
213
- # Include execution timeout if provided
214
- if timeout is not None:
215
- payload["timeout"] = str(timeout)
216
-
217
- # Ensure minimum required metadata for visibility
218
- if "metadata" not in kwargs:
219
- kwargs["metadata"] = {}
220
- if "type" not in kwargs["metadata"]:
221
- kwargs["metadata"]["type"] = "custom"
222
-
223
- # Extract IDs from tool and agent objects
224
- tool_ids = extract_ids(tools)
225
- agent_ids = extract_ids(agents)
226
-
227
- # Add tools and agents if provided
228
- if tool_ids:
229
- payload["tools"] = tool_ids
230
- if agent_ids:
231
- payload["agents"] = agent_ids
232
-
233
- # Add any additional kwargs (including language_model_id, agent_config, etc.)
234
- payload.update(kwargs)
169
+ # ------------------------------------------------------------------ #
170
+ # Renderer delegation helpers
171
+ # ------------------------------------------------------------------ #
172
+ def _get_renderer_manager(self) -> AgentRunRenderingManager:
173
+ manager = getattr(self, "_renderer_manager", None)
174
+ if manager is None:
175
+ manager = AgentRunRenderingManager(logger)
176
+ self._renderer_manager = manager
177
+ return manager
235
178
 
236
- return payload
237
-
238
- def _build_basic_update_payload(
239
- self, current_agent: "Agent", name: str | None, instruction: str | None
240
- ) -> dict[str, Any]:
241
- """Build the basic update payload with required fields."""
242
- return {
243
- "name": name if name is not None else current_agent.name,
244
- "instruction": instruction
245
- if instruction is not None
246
- else current_agent.instruction,
247
- "type": DEFAULT_AGENT_TYPE, # Required by backend
248
- "framework": DEFAULT_AGENT_FRAMEWORK, # Required by backend
249
- "version": DEFAULT_AGENT_VERSION, # Required by backend
250
- }
251
-
252
- def _handle_language_model_selection(
253
- self,
254
- update_data: dict[str, Any],
255
- current_agent: "Agent",
256
- model: str | None,
257
- language_model_id: str | None,
258
- ) -> None:
259
- """Handle language model selection with proper priority and fallbacks."""
260
- if language_model_id:
261
- # Use language_model_id if provided
262
- update_data["language_model_id"] = language_model_id
263
- elif model is not None:
264
- # Use explicit model parameter
265
- update_data["provider"] = DEFAULT_AGENT_PROVIDER
266
- update_data["model_name"] = model
267
- else:
268
- # Use current agent config or fallbacks
269
- self._set_language_model_from_current_agent(update_data, current_agent)
270
-
271
- def _set_language_model_from_current_agent(
272
- self, update_data: dict[str, Any], current_agent: "Agent"
273
- ) -> None:
274
- """Set language model from current agent config or use defaults."""
275
- if hasattr(current_agent, "agent_config") and current_agent.agent_config:
276
- agent_config = current_agent.agent_config
277
- if "lm_provider" in agent_config:
278
- update_data["provider"] = agent_config["lm_provider"]
279
- if "lm_name" in agent_config:
280
- update_data["model_name"] = agent_config["lm_name"]
281
- else:
282
- # Default fallback values
283
- update_data["provider"] = DEFAULT_AGENT_PROVIDER
284
- update_data["model_name"] = DEFAULT_MODEL
285
-
286
- def _handle_tools_and_agents(
287
- self,
288
- update_data: dict[str, Any],
289
- current_agent: "Agent",
290
- tools: list | None,
291
- agents: list | None,
292
- ) -> None:
293
- """Handle tools and agents with proper ID extraction."""
294
- # Handle tools
295
- if tools is not None:
296
- tool_ids = extract_ids(tools)
297
- update_data["tools"] = tool_ids if tool_ids else []
298
- else:
299
- update_data["tools"] = self._extract_current_tool_ids(current_agent)
179
+ def _create_renderer(
180
+ self, renderer: RichStreamRenderer | str | None, **kwargs: Any
181
+ ) -> RichStreamRenderer:
182
+ manager = self._get_renderer_manager()
183
+ verbose = kwargs.get("verbose", False)
184
+ if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
185
+ return renderer # type: ignore[return-value]
186
+ return manager.create_renderer(renderer, verbose=verbose)
300
187
 
301
- # Handle agents
302
- if agents is not None:
303
- agent_ids = extract_ids(agents)
304
- update_data["agents"] = agent_ids if agent_ids else []
305
- else:
306
- update_data["agents"] = self._extract_current_agent_ids(current_agent)
307
-
308
- def _extract_current_tool_ids(self, current_agent: "Agent") -> list[str]:
309
- """Extract tool IDs from current agent."""
310
- if current_agent.tools:
311
- return [
312
- tool["id"] if isinstance(tool, dict) else tool
313
- for tool in current_agent.tools
314
- ]
315
- return []
316
-
317
- def _extract_current_agent_ids(self, current_agent: "Agent") -> list[str]:
318
- """Extract agent IDs from current agent."""
319
- if current_agent.agents:
320
- return [
321
- agent["id"] if isinstance(agent, dict) else agent
322
- for agent in current_agent.agents
323
- ]
324
- return []
325
-
326
- def _handle_agent_config(
327
- self,
328
- update_data: dict[str, Any],
329
- current_agent: "Agent",
330
- agent_config: dict | None,
331
- ) -> None:
332
- """Handle agent_config with proper merging and cleanup."""
333
- if agent_config is not None:
334
- # Use provided agent_config, merging with current if needed
335
- update_data["agent_config"] = self._merge_agent_configs(
336
- current_agent, agent_config
337
- )
338
- elif hasattr(current_agent, "agent_config") and current_agent.agent_config:
339
- # Preserve existing agent_config
340
- update_data["agent_config"] = current_agent.agent_config.copy()
341
- else:
342
- # Default agent_config
343
- update_data["agent_config"] = {
344
- "lm_provider": DEFAULT_AGENT_PROVIDER,
345
- "lm_name": DEFAULT_MODEL,
346
- "lm_hyperparameters": {"temperature": 0.0},
347
- }
348
-
349
- # Clean LM keys from agent_config to prevent conflicts
350
- self._clean_agent_config_lm_keys(update_data)
351
-
352
- def _merge_agent_configs(self, current_agent: "Agent", new_config: dict) -> dict:
353
- """Merge current agent config with new config."""
354
- if hasattr(current_agent, "agent_config") and current_agent.agent_config:
355
- merged_config = current_agent.agent_config.copy()
356
- merged_config.update(new_config)
357
- return merged_config
358
- return new_config
359
-
360
- def _clean_agent_config_lm_keys(self, update_data: dict[str, Any]) -> None:
361
- """Remove LM keys from agent_config to prevent conflicts."""
362
- if "agent_config" in update_data and isinstance(
363
- update_data["agent_config"], dict
364
- ):
365
- agent_config = update_data["agent_config"]
366
- lm_keys_to_remove = {
367
- "lm_provider",
368
- "lm_name",
369
- "lm_base_url",
370
- "lm_hyperparameters",
371
- }
372
- for key in lm_keys_to_remove:
373
- agent_config.pop(key, None)
374
-
375
- def _finalize_update_payload(
188
+ def _process_stream_events(
376
189
  self,
377
- update_data: dict[str, Any],
378
- current_agent: "Agent",
379
- **kwargs: Any,
380
- ) -> dict[str, Any]:
381
- """Finalize the update payload with metadata and additional kwargs."""
382
- # Handle metadata preservation
383
- if hasattr(current_agent, "metadata") and current_agent.metadata:
384
- update_data["metadata"] = current_agent.metadata.copy()
385
-
386
- # Add any other kwargs (excluding already handled ones)
387
- excluded_keys = {"tools", "agents", "agent_config", "language_model_id"}
388
- for key, value in kwargs.items():
389
- if key not in excluded_keys:
390
- update_data[key] = value
391
-
392
- return update_data
190
+ stream_response: httpx.Response,
191
+ renderer: RichStreamRenderer,
192
+ timeout_seconds: float,
193
+ agent_name: str | None,
194
+ meta: dict[str, Any],
195
+ ) -> tuple[str, dict[str, Any], float | None, float | None]:
196
+ manager = self._get_renderer_manager()
197
+ return manager.process_stream_events(
198
+ stream_response,
199
+ renderer,
200
+ timeout_seconds,
201
+ agent_name,
202
+ meta,
203
+ )
393
204
 
394
- def _build_update_payload(
205
+ def _finalize_renderer(
395
206
  self,
396
- current_agent: "Agent",
397
- name: str | None = None,
398
- instruction: str | None = None,
399
- model: str | None = None,
400
- **kwargs: Any,
401
- ) -> dict[str, Any]:
402
- """Build payload for agent update with proper LM selection and current state preservation.
403
-
404
- Args:
405
- current_agent: Current agent object to update
406
- name: New agent name (None to keep current)
407
- instruction: New instruction (None to keep current)
408
- model: New language model name (None to use current or fallback)
409
- **kwargs: Additional parameters including language_model_id, agent_config, etc.
410
-
411
- Returns:
412
- Complete payload dictionary for agent update
413
-
414
- Notes:
415
- - LM exclusivity: Uses language_model_id if provided, otherwise provider/model_name
416
- - Preserves current values as defaults when new values not provided
417
- - Handles tools/agents updates with proper ID extraction
418
- """
419
- # Build basic payload
420
- update_data = self._build_basic_update_payload(current_agent, name, instruction)
421
-
422
- # Handle language model selection
423
- language_model_id = kwargs.get("language_model_id")
424
- self._handle_language_model_selection(
425
- update_data, current_agent, model, language_model_id
207
+ renderer: RichStreamRenderer,
208
+ final_text: str,
209
+ stats_usage: dict[str, Any],
210
+ started_monotonic: float | None,
211
+ finished_monotonic: float | None,
212
+ ) -> str:
213
+ manager = self._get_renderer_manager()
214
+ return manager.finalize_renderer(
215
+ renderer,
216
+ final_text,
217
+ stats_usage,
218
+ started_monotonic,
219
+ finished_monotonic,
426
220
  )
427
221
 
428
- # Handle tools and agents
429
- tools = kwargs.get("tools")
430
- agents = kwargs.get("agents")
431
- self._handle_tools_and_agents(update_data, current_agent, tools, agents)
432
-
433
- # Handle agent config
434
- agent_config = kwargs.get("agent_config")
435
- self._handle_agent_config(update_data, current_agent, agent_config)
436
-
437
- # Finalize payload
438
- return self._finalize_update_payload(update_data, current_agent, **kwargs)
439
-
440
222
  def create_agent(
441
223
  self,
442
224
  name: str,
@@ -445,28 +227,60 @@ class AgentClient(BaseClient):
445
227
  tools: list[str | Any] | None = None,
446
228
  agents: list[str | Any] | None = None,
447
229
  timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
230
+ *,
231
+ mcps: list[str | Any] | None = None,
232
+ tool_configs: Mapping[str, Any] | None = None,
448
233
  **kwargs: Any,
449
234
  ) -> "Agent":
450
235
  """Create a new agent."""
451
- # Client-side validation
452
236
  if not name or not name.strip():
453
237
  raise ValueError("Agent name cannot be empty or whitespace")
454
238
 
455
- # Validate instruction using centralized validation
456
239
  instruction = validate_agent_instruction(instruction)
457
240
 
458
- # Build payload using centralized builder
459
- payload = self._build_create_payload(
241
+ agent_type = kwargs.pop("agent_type", kwargs.pop("type", DEFAULT_AGENT_TYPE))
242
+ framework = kwargs.pop("framework", DEFAULT_AGENT_FRAMEWORK)
243
+ version = kwargs.pop("version", DEFAULT_AGENT_VERSION)
244
+ language_model_id = kwargs.pop("language_model_id", None)
245
+ provider_override = kwargs.pop("provider", None)
246
+ model_name_override = kwargs.pop("model_name", None)
247
+ account_id = kwargs.pop("account_id", None)
248
+ description = kwargs.pop("description", None)
249
+ metadata = kwargs.pop("metadata", None)
250
+ agent_config = kwargs.pop("agent_config", None)
251
+ a2a_profile = kwargs.pop("a2a_profile", None)
252
+ mcps = mcps if mcps is not None else kwargs.pop("mcps", None)
253
+ tool_configs = (
254
+ tool_configs
255
+ if tool_configs is not None
256
+ else kwargs.pop("tool_configs", None)
257
+ )
258
+
259
+ request = AgentCreateRequest(
460
260
  name=name,
461
261
  instruction=instruction,
462
262
  model=model,
263
+ language_model_id=language_model_id,
264
+ provider=provider_override,
265
+ model_name=model_name_override,
266
+ agent_type=agent_type,
267
+ framework=framework,
268
+ version=version,
269
+ account_id=account_id,
270
+ description=description,
271
+ metadata=metadata,
463
272
  tools=tools,
464
273
  agents=agents,
274
+ mcps=mcps,
275
+ tool_configs=tool_configs,
276
+ agent_config=agent_config,
465
277
  timeout=timeout,
466
- **kwargs,
278
+ a2a_profile=a2a_profile,
279
+ extras=kwargs,
467
280
  )
468
281
 
469
- # Create the agent and fetch full details
282
+ payload = request.to_payload()
283
+
470
284
  full_agent_data = self._post_then_fetch(
471
285
  id_key="id",
472
286
  post_endpoint=AGENTS_ENDPOINT,
@@ -484,20 +298,49 @@ class AgentClient(BaseClient):
484
298
  **kwargs: Any,
485
299
  ) -> "Agent":
486
300
  """Update an existing agent."""
487
- # First, get the current agent data
488
301
  current_agent = self.get_agent_by_id(agent_id)
489
302
 
490
- # Build payload using centralized builder
491
- update_data = self._build_update_payload(
492
- current_agent=current_agent,
303
+ language_model_id = kwargs.pop("language_model_id", None)
304
+ provider_override = kwargs.pop("provider", None)
305
+ model_name_override = kwargs.pop("model_name", None)
306
+ agent_type_override = kwargs.pop("agent_type", kwargs.pop("type", None))
307
+ framework_override = kwargs.pop("framework", None)
308
+ version_override = kwargs.pop("version", None)
309
+ account_id = kwargs.pop("account_id", None)
310
+ description = kwargs.pop("description", None)
311
+ metadata = kwargs.pop("metadata", None)
312
+ tools = kwargs.pop("tools", None)
313
+ tool_configs = kwargs.pop("tool_configs", None)
314
+ agents_value = kwargs.pop("agents", None)
315
+ mcps = kwargs.pop("mcps", None)
316
+ agent_config = kwargs.pop("agent_config", None)
317
+ a2a_profile = kwargs.pop("a2a_profile", None)
318
+
319
+ request = AgentUpdateRequest(
493
320
  name=name,
494
321
  instruction=instruction,
322
+ description=description,
495
323
  model=model,
496
- **kwargs,
324
+ language_model_id=language_model_id,
325
+ provider=provider_override,
326
+ model_name=model_name_override,
327
+ agent_type=agent_type_override,
328
+ framework=framework_override,
329
+ version=version_override,
330
+ account_id=account_id,
331
+ metadata=metadata,
332
+ tools=tools,
333
+ tool_configs=tool_configs,
334
+ agents=agents_value,
335
+ mcps=mcps,
336
+ agent_config=agent_config,
337
+ a2a_profile=a2a_profile,
338
+ extras=kwargs,
497
339
  )
498
340
 
499
- # Send the complete payload
500
- data = self._request("PUT", f"/agents/{agent_id}", json=update_data)
341
+ payload = request.to_payload(current_agent)
342
+
343
+ data = self._request("PUT", f"/agents/{agent_id}", json=payload)
501
344
  return Agent(**data)._set_client(self)
502
345
 
503
346
  def delete_agent(self, agent_id: str) -> None:
@@ -562,197 +405,6 @@ class AgentClient(BaseClient):
562
405
  execution_timeout = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
563
406
  return request_timeout, execution_timeout
564
407
 
565
- def _create_renderer(
566
- self, renderer: RichStreamRenderer | None, **kwargs: Any
567
- ) -> RichStreamRenderer:
568
- """Create appropriate renderer based on configuration."""
569
- if isinstance(renderer, RichStreamRenderer):
570
- return renderer
571
-
572
- verbose = kwargs.get("verbose", False)
573
-
574
- if isinstance(renderer, str):
575
- if renderer == "silent":
576
- return self._create_silent_renderer()
577
- elif renderer == "minimal":
578
- return self._create_minimal_renderer()
579
- else:
580
- return self._create_default_renderer(verbose)
581
- elif verbose:
582
- return self._create_verbose_renderer()
583
- else:
584
- return self._create_default_renderer(verbose)
585
-
586
- def _create_silent_renderer(self) -> RichStreamRenderer:
587
- """Create a silent renderer that suppresses all output."""
588
- silent_config = RendererConfig(
589
- live=False,
590
- persist_live=False,
591
- show_delegate_tool_panels=False,
592
- render_thinking=False,
593
- )
594
- return RichStreamRenderer(
595
- console=_Console(file=io.StringIO(), force_terminal=False),
596
- cfg=silent_config,
597
- verbose=False,
598
- )
599
-
600
- def _create_minimal_renderer(self) -> RichStreamRenderer:
601
- """Create a minimal renderer with basic output."""
602
- minimal_config = RendererConfig(
603
- live=False,
604
- persist_live=False,
605
- show_delegate_tool_panels=False,
606
- render_thinking=False,
607
- )
608
- return RichStreamRenderer(
609
- console=_Console(),
610
- cfg=minimal_config,
611
- verbose=False,
612
- )
613
-
614
- def _create_verbose_renderer(self) -> RichStreamRenderer:
615
- """Create a verbose renderer for detailed output."""
616
- verbose_config = RendererConfig(
617
- theme="dark",
618
- style="debug",
619
- live=False,
620
- show_delegate_tool_panels=True,
621
- append_finished_snapshots=False,
622
- )
623
- return RichStreamRenderer(
624
- console=_Console(),
625
- cfg=verbose_config,
626
- verbose=True,
627
- )
628
-
629
- def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
630
- """Create the default renderer."""
631
- if verbose:
632
- return self._create_verbose_renderer()
633
- else:
634
- default_config = RendererConfig(show_delegate_tool_panels=True)
635
- return RichStreamRenderer(console=_Console(), cfg=default_config)
636
-
637
- def _initialize_stream_metadata(self, kwargs: dict[str, Any]) -> dict[str, Any]:
638
- """Initialize stream metadata."""
639
- return {
640
- "agent_name": kwargs.get("agent_name", ""),
641
- "model": kwargs.get("model"),
642
- "run_id": None,
643
- "input_message": "", # Will be set from kwargs if available
644
- }
645
-
646
- def _capture_request_id(
647
- self,
648
- stream_response: httpx.Response,
649
- meta: dict[str, Any],
650
- renderer: RichStreamRenderer,
651
- ) -> None:
652
- """Capture request ID from response headers."""
653
- req_id = stream_response.headers.get(
654
- "x-request-id"
655
- ) or stream_response.headers.get("x-run-id")
656
- if req_id:
657
- meta["run_id"] = req_id
658
- renderer.on_start(meta)
659
-
660
- def _should_start_timer(self, ev: dict[str, Any]) -> bool:
661
- """Check if timer should be started for this event."""
662
- return "content" in ev or "status" in ev or ev.get("metadata")
663
-
664
- def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
665
- """Handle content events."""
666
- content = ev.get("content", "")
667
- if not content.startswith("Artifact received:"):
668
- return content
669
- return final_text
670
-
671
- def _handle_usage_event(
672
- self, ev: dict[str, Any], stats_usage: dict[str, Any]
673
- ) -> None:
674
- """Handle usage events."""
675
- stats_usage.update(ev.get("usage") or {})
676
-
677
- def _handle_run_info_event(
678
- self, ev: dict[str, Any], meta: dict[str, Any], renderer: RichStreamRenderer
679
- ) -> None:
680
- """Handle run info events."""
681
- if ev.get("model"):
682
- meta["model"] = ev["model"]
683
- renderer.on_start(meta)
684
- if ev.get("run_id"):
685
- meta["run_id"] = ev["run_id"]
686
- renderer.on_start(meta)
687
-
688
- def _process_single_event(
689
- self,
690
- event: dict[str, Any],
691
- renderer: RichStreamRenderer,
692
- final_text: str,
693
- stats_usage: dict[str, Any],
694
- meta: dict[str, Any],
695
- ) -> tuple[str, dict[str, Any]]:
696
- """Process a single streaming event."""
697
- try:
698
- ev = json.loads(event["data"])
699
- except json.JSONDecodeError:
700
- logger.debug("Non-JSON SSE fragment skipped")
701
- return final_text, stats_usage
702
-
703
- kind = (ev.get("metadata") or {}).get("kind")
704
- renderer.on_event(ev)
705
-
706
- # Skip artifacts from content accumulation
707
- if kind == "artifact":
708
- return final_text, stats_usage
709
-
710
- # Handle different event types
711
- if kind == "final_response" and ev.get("content"):
712
- final_text = ev.get("content", "")
713
- elif ev.get("content"):
714
- final_text = self._handle_content_event(ev, final_text)
715
- elif kind == "usage":
716
- self._handle_usage_event(ev, stats_usage)
717
- elif kind == "run_info":
718
- self._handle_run_info_event(ev, meta, renderer)
719
-
720
- return final_text, stats_usage
721
-
722
- def _process_stream_events(
723
- self,
724
- stream_response: httpx.Response,
725
- renderer: RichStreamRenderer,
726
- timeout_seconds: float,
727
- agent_name: str | None,
728
- kwargs: dict[str, Any],
729
- ) -> tuple[str, dict[str, Any], float | None, float | None]:
730
- """Process streaming events and accumulate response."""
731
- final_text = ""
732
- stats_usage = {}
733
- started_monotonic = None
734
- finished_monotonic = None
735
-
736
- meta = self._initialize_stream_metadata(kwargs)
737
- self._capture_request_id(stream_response, meta, renderer)
738
-
739
- for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
740
- # Start timer at first meaningful event
741
- if started_monotonic is None:
742
- try:
743
- ev = json.loads(event["data"])
744
- if self._should_start_timer(ev):
745
- started_monotonic = monotonic()
746
- except json.JSONDecodeError:
747
- pass
748
-
749
- final_text, stats_usage = self._process_single_event(
750
- event, renderer, final_text, stats_usage, meta
751
- )
752
-
753
- finished_monotonic = monotonic()
754
- return final_text, stats_usage, started_monotonic, finished_monotonic
755
-
756
408
  def run_agent(
757
409
  self,
758
410
  agent_id: str,
@@ -764,7 +416,6 @@ class AgentClient(BaseClient):
764
416
  **kwargs,
765
417
  ) -> str:
766
418
  """Run an agent with a message, streaming via a renderer."""
767
- # Prepare request payload and headers
768
419
  (
769
420
  payload,
770
421
  data_payload,
@@ -773,20 +424,18 @@ class AgentClient(BaseClient):
773
424
  multipart_data,
774
425
  ) = self._prepare_sync_request_data(message, files, tty, **kwargs)
775
426
 
776
- # Create renderer
777
- r = self._create_renderer(renderer, **kwargs)
427
+ render_manager = self._get_renderer_manager()
428
+ verbose = kwargs.get("verbose", False)
429
+ r = self._create_renderer(renderer, verbose=verbose)
430
+ meta = render_manager.build_initial_metadata(agent_id, message, kwargs)
431
+ render_manager.start_renderer(r, meta)
778
432
 
779
- # Initialize renderer
780
- meta = {
781
- "agent_name": kwargs.get("agent_name", agent_id),
782
- "model": kwargs.get("model"),
783
- "run_id": None,
784
- "input_message": message,
785
- }
786
- r.on_start(meta)
433
+ final_text = ""
434
+ stats_usage: dict[str, Any] = {}
435
+ started_monotonic: float | None = None
436
+ finished_monotonic: float | None = None
787
437
 
788
438
  try:
789
- # Make streaming request
790
439
  response = self.http_client.stream(
791
440
  "POST",
792
441
  f"/agents/{agent_id}/run",
@@ -799,8 +448,7 @@ class AgentClient(BaseClient):
799
448
  with response as stream_response:
800
449
  stream_response.raise_for_status()
801
450
 
802
- # Process streaming events
803
- timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
451
+ timeout_seconds = compute_timeout_seconds(kwargs)
804
452
  agent_name = kwargs.get("agent_name")
805
453
 
806
454
  (
@@ -809,7 +457,11 @@ class AgentClient(BaseClient):
809
457
  started_monotonic,
810
458
  finished_monotonic,
811
459
  ) = self._process_stream_events(
812
- stream_response, r, timeout_seconds, agent_name, kwargs
460
+ stream_response,
461
+ r,
462
+ timeout_seconds,
463
+ agent_name,
464
+ meta,
813
465
  )
814
466
 
815
467
  except KeyboardInterrupt:
@@ -823,25 +475,16 @@ class AgentClient(BaseClient):
823
475
  finally:
824
476
  raise
825
477
  finally:
826
- # Ensure cleanup
827
478
  if multipart_data:
828
479
  multipart_data.close()
829
480
 
830
- # Finalize and return result
831
- st = RunStats()
832
- st.started_at = started_monotonic or st.started_at
833
- st.finished_at = finished_monotonic or st.started_at
834
- st.usage = stats_usage
835
-
836
- # Get final content
837
- if hasattr(r, "state") and hasattr(r.state, "buffer"):
838
- rendered_text = "".join(r.state.buffer)
839
- else:
840
- rendered_text = ""
841
-
842
- final_payload = final_text or rendered_text or "No response content received."
843
- r.on_complete(st)
844
- return final_payload
481
+ return self._finalize_renderer(
482
+ r,
483
+ final_text,
484
+ stats_usage,
485
+ started_monotonic,
486
+ finished_monotonic,
487
+ )
845
488
 
846
489
  def _prepare_request_data(
847
490
  self,