xpander-sdk 2.0.144__py3-none-any.whl → 2.0.192__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 (37) hide show
  1. xpander_sdk/__init__.py +6 -0
  2. xpander_sdk/consts/api_routes.py +9 -0
  3. xpander_sdk/models/activity.py +65 -0
  4. xpander_sdk/models/compactization.py +112 -0
  5. xpander_sdk/models/deep_planning.py +18 -0
  6. xpander_sdk/models/events.py +6 -0
  7. xpander_sdk/models/frameworks.py +2 -2
  8. xpander_sdk/models/generic.py +27 -0
  9. xpander_sdk/models/notifications.py +98 -0
  10. xpander_sdk/models/orchestrations.py +271 -0
  11. xpander_sdk/modules/agents/models/agent.py +11 -5
  12. xpander_sdk/modules/agents/sub_modules/agent.py +25 -10
  13. xpander_sdk/modules/backend/__init__.py +8 -0
  14. xpander_sdk/modules/backend/backend_module.py +47 -2
  15. xpander_sdk/modules/backend/decorators/__init__.py +7 -0
  16. xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
  17. xpander_sdk/modules/backend/events_registry.py +172 -0
  18. xpander_sdk/modules/backend/frameworks/agno.py +377 -15
  19. xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
  20. xpander_sdk/modules/backend/utils/mcp_oauth.py +37 -25
  21. xpander_sdk/modules/events/decorators/__init__.py +3 -0
  22. xpander_sdk/modules/events/decorators/on_tool.py +384 -0
  23. xpander_sdk/modules/events/events_module.py +28 -1
  24. xpander_sdk/modules/tasks/models/task.py +3 -14
  25. xpander_sdk/modules/tasks/sub_modules/task.py +276 -84
  26. xpander_sdk/modules/tools_repository/models/mcp.py +1 -0
  27. xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
  28. xpander_sdk/modules/tools_repository/tools_repository_module.py +6 -2
  29. xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
  30. xpander_sdk/utils/agents/__init__.py +0 -0
  31. xpander_sdk/utils/agents/compactization_agent.py +257 -0
  32. xpander_sdk/utils/generic.py +5 -0
  33. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +224 -14
  34. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +37 -24
  35. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
  36. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
  37. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/top_level.txt +0 -0
@@ -1,24 +1,27 @@
1
1
  import asyncio
2
+ import json
2
3
  import shlex
3
4
  from os import getenv, environ
4
5
  from typing import Any, Callable, Dict, List, Optional
5
6
 
6
7
  from loguru import logger
7
-
8
+ from toon import encode as toon_encode
8
9
  from xpander_sdk import Configuration
9
10
  from xpander_sdk.models.shared import OutputFormat, ThinkMode
10
11
  from xpander_sdk.modules.agents.agents_module import Agents
11
- from xpander_sdk.modules.agents.models.agent import AgentGraphItemType
12
+ from xpander_sdk.modules.agents.models.agent import AgentGraphItemType, LLMReasoningEffort
12
13
  from xpander_sdk.modules.agents.sub_modules.agent import Agent
13
14
  from xpander_sdk.modules.backend.utils.mcp_oauth import authenticate_mcp_server
14
15
  from xpander_sdk.modules.tasks.sub_modules.task import Task
15
16
  from xpander_sdk.modules.tools_repository.models.mcp import (
17
+ MCPOAuthGetTokenGenericResponse,
16
18
  MCPOAuthGetTokenResponse,
17
19
  MCPOAuthResponseType,
18
20
  MCPServerAuthType,
19
21
  MCPServerTransport,
20
22
  MCPServerType,
21
23
  )
24
+ from xpander_sdk.modules.tools_repository.models.tool_invocation_result import ToolInvocationResult
22
25
  from xpander_sdk.modules.tools_repository.sub_modules.tool import Tool
23
26
  from xpander_sdk.modules.tools_repository.utils.schemas import build_model_from_schema
24
27
  from agno.agent import Agent as AgnoAgent
@@ -34,13 +37,14 @@ async def build_agent_args(
34
37
  override: Optional[Dict[str, Any]] = None,
35
38
  tools: Optional[List[Callable]] = None,
36
39
  is_async: Optional[bool] = True,
40
+ auth_events_callback: Optional[Callable] = None,
37
41
  ) -> Dict[str, Any]:
38
42
  model = _load_llm_model(agent=xpander_agent, override=override)
39
43
  args: Dict[str, Any] = {
40
44
  "id": xpander_agent.id,
41
45
  "store_events": True
42
46
  }
43
-
47
+
44
48
  _configure_output(args=args, agent=xpander_agent, task=task)
45
49
  _configure_session_storage(args=args, agent=xpander_agent, task=task)
46
50
  _configure_agentic_memory(args=args, agent=xpander_agent, task=task)
@@ -53,7 +57,7 @@ async def build_agent_args(
53
57
  # Configure pre-hooks (guardrails, etc.)
54
58
  _configure_pre_hooks(args=args, agent=xpander_agent, model=model)
55
59
 
56
- args["tools"] = await _resolve_agent_tools(agent=xpander_agent, task=task)
60
+ args["tools"] = await _resolve_agent_tools(agent=xpander_agent, task=task, auth_events_callback=auth_events_callback)
57
61
 
58
62
 
59
63
  if tools and len(tools) != 0:
@@ -100,7 +104,7 @@ async def build_agent_args(
100
104
  # convert to members
101
105
  members = await asyncio.gather(
102
106
  *[
103
- build_agent_args(xpander_agent=sub_agent, override=override, task=task)
107
+ build_agent_args(xpander_agent=sub_agent, override=override, task=task, is_async=is_async, auth_events_callback=auth_events_callback)
104
108
  for sub_agent in sub_agents
105
109
  ]
106
110
  )
@@ -108,7 +112,7 @@ async def build_agent_args(
108
112
  args.update(
109
113
  {
110
114
  "members": [
111
- AgnoAgent(**member) if "id" in member else AgnoTeam(**member)
115
+ AgnoAgent(**member) if "members" not in member else AgnoTeam(**member)
112
116
  for member in members
113
117
  ],
114
118
  "add_member_tools_to_context": True,
@@ -199,6 +203,19 @@ async def build_agent_args(
199
203
  except Exception:
200
204
  pass
201
205
 
206
+ # try toon optimization if compression enabled
207
+ if xpander_agent and xpander_agent and xpander_agent.agno_settings and xpander_agent.agno_settings.tool_calls_compression and xpander_agent.agno_settings.tool_calls_compression.enabled and not function_name.startswith("xp"):
208
+ try:
209
+ if isinstance(result, ToolInvocationResult):
210
+ json_result = json.loads(result.result) if isinstance(result.result, str) else result.result
211
+ result.result = toon_encode(json_result)
212
+ elif hasattr(result, "content"):
213
+ json_result = json.loads(result.content)
214
+ result.content = toon_encode(json_result)
215
+ except Exception:
216
+ pass
217
+
218
+
202
219
  # Return the result
203
220
  return result
204
221
 
@@ -213,10 +230,349 @@ async def build_agent_args(
213
230
  if args["model"] and args["model"].id and args["model"].id.startswith("gpt-5"):
214
231
  del args["model"].temperature
215
232
 
233
+ # configure deep planning guidance
234
+ _configure_deep_planning_guidance(args=args, agent=xpander_agent, task=task)
216
235
  return args
217
236
 
237
+ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task: Optional[Task]) -> None:
238
+ if args and agent and task and agent.deep_planning and task.deep_planning.enabled == True:
239
+ task.reload() # reload the task - get latest version
240
+ # add instructions guidance
241
+ if not "instructions" in args:
242
+ args["instructions"] = ""
243
+
244
+ args["instructions"] += """\n
245
+ <important_planning_instructions>
246
+ ## **Deep Planning Tools - Essential for Multi-Step Tasks**
247
+
248
+ **ABSOLUTE RULE - READ THIS FIRST**:
249
+
250
+ IF YOU ARE ABOUT TO WRITE A QUESTION TO THE USER, STOP AND CHECK:
251
+ - Did I call xpstart_execution_plan?
252
+ - YES → Use xpask_for_information tool (DO NOT write the question in your response)
253
+ - NO → You can ask directly
254
+
255
+ SIMPLE RULE:
256
+ - BEFORE calling xpstart_execution_plan: You CAN ask questions directly in your response
257
+ - AFTER calling xpstart_execution_plan: You are FORBIDDEN from writing questions - use xpask_for_information tool
258
+
259
+ Once execution has started, writing things like "Before I proceed, I need to know..." or "I need clarification on..."
260
+ or "Please choose one of the following..." in your response text is STRICTLY PROHIBITED and will cause execution failures.
261
+
262
+ MANDATORY CHECK BEFORE ASKING: Have I started the plan? YES = use tool | NO = can ask directly
263
+
264
+ When handling complex tasks with multiple steps, use these planning tools to track progress systematically.
265
+
266
+ ### **Core Workflow**
267
+ 1. **CREATE** plan at the start (`xpcreate_agent_plan`)
268
+ 2. **START** plan execution (`xpstart_execution_plan`) - MANDATORY to enable enforcement
269
+ 3. **CHECK** plan before each action (`xpget_agent_plan`) - note the FULL UUID of the task you'll work on
270
+ 4. **DO THE WORK** for one task
271
+ 5. **COMPLETE** task IMMEDIATELY - call `xpcomplete_agent_plan_items` with the FULL UUID RIGHT AFTER finishing work (DO NOT DELAY!)
272
+ 6. **ASK** user for info if needed (`xpask_for_information`) - MANDATORY if you need input or want to pause
273
+ 7. Repeat steps 3-6 until all tasks are done
274
+
275
+ **UUID REQUIREMENT**: All task IDs are full UUIDs (e.g., '35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92'). You MUST use the exact complete UUID string from the plan when marking tasks complete.
276
+
277
+ **CRITICAL RULES - ABSOLUTE REQUIREMENTS**:
278
+ - Mark each task complete THE MOMENT you finish it - not later, not at the end, RIGHT AWAY!
279
+ - **CHECK BEFORE ASKING**: Did you call xpstart_execution_plan?
280
+ - If YES: Use xpask_for_information tool ONLY (never write questions in response)
281
+ - If NO: Can ask directly
282
+ - **AFTER plan starts: Writing questions in your response is FORBIDDEN** - violates execution protocol
283
+ - If you called xpstart_execution_plan and then write "I need clarification" or "Before I proceed" or "Please choose", you are VIOLATING THE PROTOCOL
284
+ - AFTER xpstart_execution_plan: The ONLY way to ask users questions is through `xpask_for_information` tool - ZERO EXCEPTIONS!
285
+
286
+ ---
287
+
288
+ ### **Tool Reference**
289
+
290
+ #### **1. xpcreate_agent_plan** - Create Initial Plan
291
+ **When to use**: At the very start of any multi-step task, ONLY if no plan exists yet
292
+ **Note**: After creating a plan, you MUST call `xpstart_execution_plan` to begin enforcement
293
+
294
+ **How to use**:
295
+ - Pass an array of task objects (NOT strings)
296
+ - Each task must have a `title` field with short, explanatory description
297
+ - Example format:
298
+ ```json
299
+ {
300
+ "body_params": {
301
+ "tasks": [
302
+ {"title": "Research API requirements"},
303
+ {"title": "Design data schema"},
304
+ {"title": "Implement endpoints"},
305
+ {"title": "Write tests"},
306
+ {"title": "Deploy to staging"}
307
+ ]
308
+ }
309
+ }
310
+ ```
311
+ **Important**:
312
+ - Include ALL steps needed from start to finish
313
+ - Each title should be clear and actionable (3-6 words)
314
+ - Do NOT pass plain strings like `["Task 1", "Task 2"]` - must be objects!
315
+
316
+ ---
317
+
318
+ #### **2. xpget_agent_plan** - View Current Plan
319
+ **When to use**: Before deciding what to do next; to check progress
320
+
321
+ **Returns**:
322
+ - All tasks with their IDs, titles, and completion status
323
+ - Use this to know what's done and what remains
324
+
325
+ **No parameters needed** - just call the tool
326
+
327
+ ---
328
+
329
+ #### **3. xpcomplete_agent_plan_items** - Mark Task(s) Complete
330
+ **When to use**: **IMMEDIATELY** after finishing one or more tasks (NOT before, NOT later, RIGHT NOW!)
331
+
332
+ **How to use**:
333
+ ```json
334
+ // Single task
335
+ {
336
+ "body_params": {
337
+ "ids": ["35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92"]
338
+ }
339
+ }
340
+
341
+ // Multiple tasks (when finishing related tasks together)
342
+ {
343
+ "body_params": {
344
+ "ids": ["35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92", "8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c", "f2b9d1c7-3e8a-4b6f-9d2c-5a7e1f4b8c3d"]
345
+ }
346
+ }
347
+ ```
348
+ **🚨 CRITICAL - NON-NEGOTIABLE RULES**:
349
+ - IDs must be the FULL UUID strings (e.g., '35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92') from the plan's 'id' field
350
+ - Get these exact UUID strings from xpget_agent_plan before calling this tool
351
+ - Call THIS TOOL the INSTANT you finish task(s)
352
+ - Can mark single or multiple tasks complete in one call
353
+ - Use multiple FULL UUIDs when finishing related tasks at the same time
354
+ - DO NOT use shortened IDs, abbreviations, or partial UUIDs - must be complete UUID strings
355
+ - DO NOT postpone marking completion
356
+ - DO NOT be lazy - mark it complete RIGHT AFTER the work is done
357
+ - This is MANDATORY for progress tracking and continuation
358
+ - If you finish a task and don't mark it complete immediately, you are doing it WRONG
359
+
360
+ **Pattern**: Finish work → Get task UUID from plan → IMMEDIATELY call xpcomplete_agent_plan_items with FULL UUID → Move to next task
361
+
362
+ ---
363
+
364
+ #### **4. xpadd_new_agent_plan_item** - Add Discovered Task
365
+ **When to use**: When you discover additional work needed during execution
366
+
367
+ **How to use**:
368
+ ```json
369
+ {
370
+ "body_params": {
371
+ "title": "Validate input schemas",
372
+ "completed": false
373
+ }
374
+ }
375
+ ```
376
+ ---
377
+
378
+ #### **5. xpupdate_agent_plan_item** - Modify Existing Task
379
+ **When to use**: When task details change or need clarification
380
+
381
+ **How to use**:
382
+ ```json
383
+ {
384
+ "body_params": {
385
+ "id": "task-uuid",
386
+ "title": "Updated description",
387
+ "completed": true
388
+ }
389
+ }
390
+ ```
391
+ (All fields optional except `id`)
392
+
393
+ ---
218
394
 
219
- def _load_llm_model(agent: Agent, override: Optional[Dict[str, Any]]) -> Any:
395
+ #### **6. xpdelete_agent_plan_item** - Remove Task
396
+ **When to use**: When a task becomes unnecessary or redundant
397
+
398
+ **How to use**:
399
+ ```json
400
+ {
401
+ "body_params": {
402
+ "id": "task-uuid"
403
+ }
404
+ }
405
+ ```
406
+ ---
407
+
408
+ #### **7. xpstart_execution_plan** - Start Plan Execution
409
+ **When to use**: Immediately after creating a plan with `xpcreate_agent_plan`
410
+ **CRITICAL**: Must be called to enable enforcement mode before executing tasks
411
+
412
+ **How to use**:
413
+ ```json
414
+ {
415
+ "body_params": {}
416
+ }
417
+ ```
418
+ **No parameters needed** - just call after plan creation
419
+
420
+ **What it does**:
421
+ - Marks plan as "started"
422
+ - Enables enforcement mode if `enforce` flag is true
423
+ - Allows plan execution to proceed
424
+
425
+ ---
426
+
427
+ #### **8. xpask_for_information** - Ask User a Question
428
+ **When to use**: **MANDATORY AFTER PLAN STARTS** when you need ANY of the following:
429
+ - Information from the user
430
+ - Clarification on requirements
431
+ - Approval before proceeding
432
+ - To pause execution for user input
433
+
434
+ **PREREQUISITE**: Plan must be started (running) first
435
+
436
+ **CRITICAL RULE**: Once the plan has started, if you need user input or want to pause, you MUST use this tool.
437
+ DO NOT just respond with questions in your regular output after plan starts - that breaks execution flow!
438
+
439
+ **NOTE**: BEFORE starting the plan (before xpstart_execution_plan), you CAN ask questions directly if needed.
440
+
441
+ **How to use**:
442
+ ```json
443
+ {
444
+ "body_params": {
445
+ "question": "What is the customer email address?"
446
+ }
447
+ }
448
+ ```
449
+
450
+ **What it does**:
451
+ - Properly pauses plan enforcement and sets `question_raised` flag
452
+ - Delivers question to the user through the correct channel
453
+ - Manages execution state for proper continuation
454
+ - Allows resuming execution after user responds
455
+
456
+ **Why this matters**: Using this tool ensures execution can be properly continued with full context.
457
+ Just responding with a question will NOT pause execution correctly!
458
+
459
+ ---
460
+
461
+ ### **Best Practices**
462
+
463
+ ✅ **DO:**
464
+ - Create comprehensive plans with ALL necessary steps
465
+ - **START** the plan with `xpstart_execution_plan` after creating it
466
+ - Use descriptive, actionable task titles
467
+ - Check plan before each action to stay oriented
468
+ - **Always use FULL UUID strings when marking tasks complete** (e.g., '35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92')
469
+ - **Get the exact UUID from xpget_agent_plan** - copy the full 'id' field value
470
+ - **Mark tasks complete THE INSTANT you finish them - NO DELAYS, NO EXCEPTIONS**
471
+ - **Can mark multiple tasks at once if finished together** (e.g., related tasks done simultaneously)
472
+ - **ALWAYS use `xpask_for_information` when you need user input or want to pause**
473
+ - Call plan tools **sequentially** (one at a time, never in parallel)
474
+ - Follow the pattern: DO WORK → GET UUID → MARK COMPLETE WITH FULL UUID → NEXT TASK
475
+
476
+ ❌ **DON'T - THESE ARE FORBIDDEN:**
477
+ - Mark tasks complete before they're actually done
478
+ - **Use shortened, partial, or abbreviated task IDs** - MUST use complete UUID strings!
479
+ - **Use made-up or guessed UUIDs** - MUST get exact UUID from xpget_agent_plan!
480
+ - **Be lazy and wait to mark tasks complete later** (FORBIDDEN!)
481
+ - **Postpone marking completion to batch at the end** (WRONG! Mark immediately when done!)
482
+ - **AFTER plan starts: NEVER write questions in your response text** ("Before I proceed...", "I need clarification...", etc.)
483
+ - **AFTER plan starts: NEVER respond with questions - ONLY use xpask_for_information tool** (ABSOLUTE RULE!)
484
+ - Pass plain string arrays - must be objects with `title` field
485
+ - Call plan tools in parallel with each other
486
+ - Skip checking the plan between major steps
487
+ - Postpone marking completion "until later" - there is no later, only NOW
488
+
489
+ **REMEMBER**: Once execution started, any text like "I need to know", "Before I proceed", "I need clarification" means you MUST call xpask_for_information instead!
490
+ **EXCEPTION**: Before starting the plan, you CAN ask questions directly if needed for planning.
491
+
492
+ ---
493
+
494
+ ### **Example Complete Workflow**
495
+
496
+ ```
497
+ 1. User: "Build a REST API for user management"
498
+
499
+ 2. Call: xpcreate_agent_plan
500
+ tasks: [
501
+ {"title": "Design user schema"},
502
+ {"title": "Create database migration"},
503
+ {"title": "Implement CRUD endpoints"},
504
+ {"title": "Add authentication"},
505
+ {"title": "Write integration tests"}
506
+ ]
507
+
508
+ 3. Call: xpstart_execution_plan
509
+ → Plan now started, enforcement enabled (if enforce=true)
510
+
511
+ 4. Call: xpget_agent_plan
512
+ → See: Task 1 (ID: 35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92) - Design user schema - Not complete
513
+
514
+ 5. [Realize need user input] Call: xpask_for_information
515
+ question: "Which database should we use - PostgreSQL or MySQL?"
516
+ → question_raised=true, waiting for response
517
+
518
+ 6. [After user responds, DO THE WORK: Design schema]
519
+
520
+ 7. ⚠️ IMMEDIATELY Call: xpcomplete_agent_plan_items
521
+ ids: ["35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92"]
522
+ → MARKED COMPLETE RIGHT AFTER FINISHING - NOT DELAYED! Used FULL UUID from plan!
523
+
524
+ 8. Call: xpget_agent_plan
525
+ → See: Task 1 ✓ complete, Task 2 (ID: 8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c) - Create database migration - Not complete
526
+
527
+ 9. [DO THE WORK: Create migration file]
528
+
529
+ 10. ⚠️ IMMEDIATELY Call: xpcomplete_agent_plan_items
530
+ ids: ["8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c"]
531
+ → MARKED COMPLETE RIGHT AWAY! Used FULL UUID from plan!
532
+
533
+ // Alternative: If tasks 3 and 4 are done together, can batch complete:
534
+ 11a. [DO THE WORK: Implement endpoints AND add auth together]
535
+ 11b. ⚠️ IMMEDIATELY Call: xpcomplete_agent_plan_items
536
+ ids: ["f2b9d1c7-3e8a-4b6f-9d2c-5a7e1f4b8c3d", "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]
537
+ → Both marked complete at once! Used FULL UUIDs from plan!
538
+
539
+ 12. [Continue this pattern: GET PLAN → DO WORK → MARK COMPLETE IMMEDIATELY → REPEAT]
540
+ ```
541
+
542
+ ### **WRONG WAY - ANTI-PATTERN (DO NOT DO THIS)**
543
+
544
+ ```
545
+ ❌ WRONG:
546
+ 1. Call: xpcreate_agent_plan
547
+ 2. Call: xpstart_execution_plan ← PLAN IS NOW STARTED!
548
+ 3. Respond: "Before I proceed, I need clarification on..." ← WRONG! FORBIDDEN!
549
+
550
+ ✅ CORRECT:
551
+ 1. Call: xpcreate_agent_plan
552
+ 2. Call: xpstart_execution_plan ← PLAN IS NOW STARTED!
553
+ 3. Call: xpask_for_information with question ← CORRECT! Use the tool!
554
+ ```
555
+
556
+ **KEY POINT**: Once you call `xpstart_execution_plan`, the execution mode changes.
557
+ You CANNOT write questions in your response anymore. You MUST use the tool.
558
+
559
+ **Golden Rule**: FINISH TASK → MARK COMPLETE INSTANTLY → MOVE TO NEXT TASK. No delays, no batching, no "I'll do it later"!
560
+ </important_planning_instructions>
561
+ """
562
+
563
+ # add the expected output guidance
564
+ if not "expected_output" in args:
565
+ args["expected_output"] = ""
566
+ args["expected_output"] += "\nAll planned tasks completed and marked as done."
567
+
568
+ # add the plan to additional_context
569
+ if not "additional_context" in args:
570
+ args["additional_context"] = ""
571
+
572
+ plan_str = task.deep_planning.model_dump_json() if task.deep_planning and task.deep_planning.enabled and len(task.deep_planning.tasks) != 0 else "No execution plan, please generate"
573
+ args["additional_context"] += f" \n Current execution plan: {plan_str}"
574
+
575
+ def _load_llm_model(agent: Agent, override: Optional[Dict[str, Any]] = {}) -> Any:
220
576
  """
221
577
  Load and configure the appropriate LLM model based on the agent's provider configuration.
222
578
 
@@ -278,6 +634,10 @@ def _load_llm_model(agent: Agent, override: Optional[Dict[str, Any]]) -> Any:
278
634
  return env_llm_key or agent.llm_credentials.value
279
635
 
280
636
  llm_args = {}
637
+
638
+ if agent.llm_reasoning_effort and agent.llm_reasoning_effort != LLMReasoningEffort.Medium and agent.model_name and "gpt-5" in agent.model_name.lower():
639
+ llm_args = { "reasoning_effort": agent.llm_reasoning_effort.value }
640
+
281
641
  if agent.llm_api_base and len(agent.llm_api_base) != 0:
282
642
  llm_args["base_url"] = agent.llm_api_base
283
643
 
@@ -427,7 +787,7 @@ def _configure_tool_calls_compression(
427
787
  args["compression_manager"] = CompressionManager(
428
788
  compress_tool_results=True,
429
789
  compress_tool_results_limit=agent.agno_settings.tool_calls_compression.threshold,
430
- compress_tool_call_instructions=agent.agno_settings.tool_calls_compression.instructions,
790
+ compress_tool_call_instructions="never compress ids, keep them full. "+(agent.agno_settings.tool_calls_compression.instructions if agent.agno_settings.tool_calls_compression.instructions else ""),
431
791
  )
432
792
 
433
793
  def _configure_agentic_memory(
@@ -442,7 +802,7 @@ def _configure_agentic_memory(
442
802
  args["memory_manager"] = MemoryManager(delete_memories=True,clear_memories=True)
443
803
  args["enable_agentic_memory"] = agent.agno_settings.agentic_memory
444
804
 
445
- if agent_memories_enabled:
805
+ if agent_memories_enabled and not agent.is_a_team:
446
806
  args["add_culture_to_context"] = True
447
807
 
448
808
  if agent.agno_settings.agentic_culture:
@@ -533,7 +893,7 @@ def _configure_pre_hooks(args: Dict[str, Any], agent: Agent, model: Any) -> None
533
893
  args["pre_hooks"].append(openai_moderation_guardrail)
534
894
 
535
895
 
536
- async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> List[Any]:
896
+ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None, auth_events_callback: Optional[Callable] = None) -> List[Any]:
537
897
  mcp_servers = agent.mcp_servers
538
898
 
539
899
  # combine task mcps and agent mcps
@@ -579,6 +939,7 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
579
939
  ),
580
940
  include_tools=mcp.allowed_tools or None,
581
941
  timeout_seconds=120,
942
+ tool_name_prefix="mcp_tool"
582
943
  )
583
944
  )
584
945
  elif mcp.url:
@@ -596,7 +957,9 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
596
957
  if not task.input.user or not task.input.user.id:
597
958
  raise ValueError("MCP server with OAuth authentication detected but user id not set on the task (task.input.user.id)")
598
959
 
599
- auth_result: MCPOAuthGetTokenResponse = await authenticate_mcp_server(mcp_server=mcp,task=task,user_id=task.input.user.id)
960
+ auth_result: MCPOAuthGetTokenResponse = await authenticate_mcp_server(mcp_server=mcp,task=task,user_id=task.input.user.id, auth_events_callback=auth_events_callback)
961
+ if auth_result and auth_result.data and isinstance(auth_result.data, MCPOAuthGetTokenGenericResponse) and auth_result.data.message:
962
+ raise ValueError(f"MCP authentication failed: {auth_result.data.message}")
600
963
  if not auth_result:
601
964
  raise ValueError("MCP Server authentication failed")
602
965
  if auth_result.type != MCPOAuthResponseType.TOKEN_READY:
@@ -612,10 +975,9 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
612
975
  transport=transport,
613
976
  server_params=params_cls(url=mcp.url, headers=mcp.headers),
614
977
  include_tools=mcp.allowed_tools or None,
615
- timeout_seconds=120
978
+ timeout_seconds=120,
979
+ tool_name_prefix="mcp_tool"
616
980
  )
617
981
  )
618
982
 
619
- return agent.tools.functions + await asyncio.gather(
620
- *[mcp.__aenter__() for mcp in mcp_tools]
621
- )
983
+ return agent.tools.functions + mcp_tools
@@ -10,6 +10,7 @@ async def dispatch_get_args(
10
10
  override: Optional[Dict[str, Any]] = None,
11
11
  tools: Optional[List[Callable]] = None,
12
12
  is_async: Optional[bool] = True,
13
+ auth_events_callback: Optional[Callable] = None,
13
14
  ) -> Dict[str, Any]:
14
15
  """
15
16
  Dispatch to the correct framework-specific argument resolver.
@@ -20,6 +21,7 @@ async def dispatch_get_args(
20
21
  override (Optional[Dict[str, Any]]): Dict of override values.
21
22
  tools (Optional[List[Callable]]): Optional additional tools to be added to the agent arguments.
22
23
  is_async (Optional[bool]): Is in Async Context?.
24
+ auth_events_callback (Optional[Callable]): Optional callback function (async or sync) that receives (agent, task, event) for authentication events only.
23
25
 
24
26
  Returns:
25
27
  Dict[str, Any]: Arguments for instantiating the framework agent.
@@ -28,7 +30,7 @@ async def dispatch_get_args(
28
30
  match agent.framework:
29
31
  case Framework.Agno:
30
32
  from .agno import build_agent_args
31
- return await build_agent_args(xpander_agent=agent, task=task, override=override, tools=tools, is_async=is_async)
33
+ return await build_agent_args(xpander_agent=agent, task=task, override=override, tools=tools, is_async=is_async, auth_events_callback=auth_events_callback)
32
34
  # case Framework.Langchain: # PLACEHOLDER
33
35
  # from .langchain import build_agent_args
34
36
  # return await build_agent_args(xpander_agent=agent, task=task, override=override)
@@ -1,57 +1,69 @@
1
1
  import asyncio
2
2
  from datetime import datetime, timezone
3
+ from typing import Callable, Optional
3
4
  from loguru import logger
4
5
  from xpander_sdk.consts.api_routes import APIRoute
5
6
  from xpander_sdk.core.xpander_api_client import APIClient
6
7
  from xpander_sdk.models.events import TaskUpdateEventType
8
+ from xpander_sdk.modules.agents.sub_modules.agent import Agent
7
9
  from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
8
10
  from xpander_sdk.modules.tools_repository.models.mcp import MCPOAuthGetTokenGenericResponse, MCPOAuthGetTokenResponse, MCPOAuthResponseType, MCPServerDetails
11
+ from xpander_sdk.modules.backend.events_registry import EventsRegistry
9
12
 
10
13
  POLLING_INTERVAL = 1 # every 1s
11
- MAX_WAIT_FOR_LOGIN = 300 # 5 mintutes
14
+ MAX_WAIT_FOR_LOGIN = 600 # 10 mintutes
12
15
 
13
- async def push_event(task: Task, event: TaskUpdateEvent):
16
+ async def push_event(task: Task, event: TaskUpdateEvent, event_type: TaskUpdateEventType, auth_events_callback: Optional[Callable] = None):
14
17
  client = APIClient(configuration=task.configuration)
18
+
19
+ evt = TaskUpdateEvent(
20
+ task_id=task.id,
21
+ organization_id=task.organization_id,
22
+ time=datetime.now(timezone.utc).isoformat(),
23
+ type=event_type,
24
+ data=event
25
+ )
26
+
15
27
  await client.make_request(
16
28
  path=APIRoute.PushExecutionEventToQueue.format(task_id=task.id),
17
29
  method="POST",
18
- payload=[
19
- TaskUpdateEvent(
20
- task_id=task.id,
21
- organization_id=task.organization_id,
22
- time=datetime.now(timezone.utc).isoformat(),
23
- type=TaskUpdateEventType.AuthEvent,
24
- data=event
25
- ).model_dump_safe()
26
- ]
30
+ payload=[evt.model_dump_safe()]
27
31
  )
32
+
33
+ # Invoke both explicit callback and registered handlers
34
+ # 1. Call explicit callback if provided
35
+ if auth_events_callback:
36
+ if asyncio.iscoroutinefunction(auth_events_callback):
37
+ await auth_events_callback(task.configuration.state.agent, task, evt)
38
+ else:
39
+ auth_events_callback(task.configuration.state.agent, task, evt)
40
+
41
+ # 2. Always invoke registered handlers from EventsRegistry
42
+ registry = EventsRegistry()
43
+ if registry.has_auth_handlers():
44
+ await registry.invoke_auth_handlers(task.configuration.state.agent, task, evt)
28
45
 
29
- async def get_token(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
46
+ async def get_token(mcp_server: MCPServerDetails, task: Task, user_id: str, return_result: Optional[bool] = False, generate_login_url: Optional[bool] = True) -> MCPOAuthGetTokenResponse:
30
47
  client = APIClient(configuration=task.configuration)
31
48
  result: MCPOAuthGetTokenResponse = await client.make_request(
32
49
  path=APIRoute.GetUserMCPAuthToken.format(agent_id=task.agent_id, user_id=user_id),
33
50
  method="POST",
51
+ query={"generate_login_url": generate_login_url},
34
52
  payload=mcp_server.model_dump(),
35
53
  model=MCPOAuthGetTokenResponse
36
54
  )
37
- if result.type == MCPOAuthResponseType.TOKEN_READY:
55
+ if result.type == MCPOAuthResponseType.TOKEN_READY or return_result:
38
56
  return result
39
57
 
40
58
  return None
41
59
 
42
- async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
60
+ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user_id: str, auth_events_callback: Optional[Callable] = None) -> MCPOAuthGetTokenResponse:
43
61
  try:
44
62
  logger.info(f"Authenticating MCP Server {mcp_server.url}")
45
63
 
46
64
  user_identifier = user_id if mcp_server.share_user_token_across_other_agents else f"{task.agent_id}_{user_id}"
47
65
 
48
- client = APIClient(configuration=task.configuration)
49
- result: MCPOAuthGetTokenResponse = await client.make_request(
50
- path=APIRoute.GetUserMCPAuthToken.format(agent_id=task.agent_id, user_id=user_identifier),
51
- method="POST",
52
- payload=mcp_server.model_dump(),
53
- model=MCPOAuthGetTokenResponse
54
- )
66
+ result: MCPOAuthGetTokenResponse = await get_token(mcp_server=mcp_server, task=task, user_id=user_identifier, return_result=True)
55
67
 
56
68
  if not result:
57
69
  raise Exception("Invalid response")
@@ -59,7 +71,7 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
59
71
  if result.type == MCPOAuthResponseType.LOGIN_REQUIRED:
60
72
  logger.info(f"Initiating login for MCP Server {mcp_server.url}")
61
73
  # Notify user about login requirement
62
- await push_event(task=task, event=result)
74
+ await push_event(task=task, event=result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
63
75
 
64
76
  # Poll for token with timeout
65
77
  elapsed_time = 0
@@ -68,12 +80,12 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
68
80
  elapsed_time += POLLING_INTERVAL
69
81
 
70
82
  # Check for token
71
- token_result = await get_token(mcp_server=mcp_server, task=task, user_id=user_identifier)
83
+ token_result = await get_token(mcp_server=mcp_server, task=task, user_id=user_identifier, generate_login_url=False)
72
84
  if token_result and token_result.type == MCPOAuthResponseType.TOKEN_READY:
73
85
  logger.info(f"Successful login for MCP Server {mcp_server.url}")
74
86
  redacted_token_result = MCPOAuthGetTokenResponse(**token_result.model_dump_safe())
75
87
  redacted_token_result.data.access_token = "REDACTED"
76
- await push_event(task=task, event=redacted_token_result)
88
+ await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
77
89
  return token_result
78
90
 
79
91
  # Timeout reached
@@ -83,7 +95,7 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
83
95
  logger.info(f"Token ready for MCP Server {mcp_server.url}")
84
96
  redacted_token_result = MCPOAuthGetTokenResponse(**result.model_dump_safe())
85
97
  redacted_token_result.data.access_token = "REDACTED"
86
- await push_event(task=task, event=redacted_token_result)
98
+ await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
87
99
 
88
100
  return result
89
101
  except Exception as e:
@@ -0,0 +1,3 @@
1
+ from .on_tool import on_tool_before, on_tool_after, on_tool_error
2
+
3
+ __all__ = ["on_tool_before", "on_tool_after", "on_tool_error"]