xpander-sdk 2.0.161__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 (33) hide show
  1. xpander_sdk/__init__.py +6 -0
  2. xpander_sdk/consts/api_routes.py +8 -0
  3. xpander_sdk/models/compactization.py +112 -0
  4. xpander_sdk/models/events.py +3 -0
  5. xpander_sdk/models/frameworks.py +2 -2
  6. xpander_sdk/models/generic.py +27 -0
  7. xpander_sdk/models/notifications.py +98 -0
  8. xpander_sdk/models/orchestrations.py +271 -0
  9. xpander_sdk/modules/agents/models/agent.py +7 -4
  10. xpander_sdk/modules/agents/sub_modules/agent.py +18 -10
  11. xpander_sdk/modules/backend/__init__.py +8 -0
  12. xpander_sdk/modules/backend/backend_module.py +47 -2
  13. xpander_sdk/modules/backend/decorators/__init__.py +7 -0
  14. xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
  15. xpander_sdk/modules/backend/events_registry.py +172 -0
  16. xpander_sdk/modules/backend/frameworks/agno.py +176 -54
  17. xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
  18. xpander_sdk/modules/backend/utils/mcp_oauth.py +36 -24
  19. xpander_sdk/modules/events/decorators/__init__.py +3 -0
  20. xpander_sdk/modules/events/decorators/on_tool.py +384 -0
  21. xpander_sdk/modules/events/events_module.py +9 -3
  22. xpander_sdk/modules/tasks/models/task.py +3 -14
  23. xpander_sdk/modules/tasks/sub_modules/task.py +54 -20
  24. xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
  25. xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
  26. xpander_sdk/utils/agents/__init__.py +0 -0
  27. xpander_sdk/utils/agents/compactization_agent.py +257 -0
  28. xpander_sdk/utils/generic.py +5 -0
  29. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +97 -13
  30. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +33 -22
  31. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
  32. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
  33. {xpander_sdk-2.0.161.dist-info → xpander_sdk-2.0.192.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,11 @@
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
@@ -13,12 +14,14 @@ 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
 
@@ -219,6 +236,7 @@ async def build_agent_args(
219
236
 
220
237
  def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task: Optional[Task]) -> None:
221
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
222
240
  # add instructions guidance
223
241
  if not "instructions" in args:
224
242
  args["instructions"] = ""
@@ -227,23 +245,51 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
227
245
  <important_planning_instructions>
228
246
  ## **Deep Planning Tools - Essential for Multi-Step Tasks**
229
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
+
230
264
  When handling complex tasks with multiple steps, use these planning tools to track progress systematically.
231
265
 
232
266
  ### **Core Workflow**
233
- 1. **CREATE** plan at the start (`xpcreate-agent-plan`)
234
- 2. **START** plan execution (`xpstart-execution-plan`) - MANDATORY to enable enforcement
235
- 3. **CHECK** plan before each action (`xpget-agent-plan`)
236
- 4. **COMPLETE** task immediately after finishing it (`xpcomplete-agent-plan-item`)
237
- 5. **ASK** user for info if needed (`xpask-for-information`)
238
- 6. Repeat steps 3-5 until all tasks are done
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!
239
285
 
240
286
  ---
241
287
 
242
288
  ### **Tool Reference**
243
289
 
244
- #### **1. xpcreate-agent-plan** - Create Initial Plan
290
+ #### **1. xpcreate_agent_plan** - Create Initial Plan
245
291
  **When to use**: At the very start of any multi-step task, ONLY if no plan exists yet
246
- **Note**: After creating a plan, you MUST call `xpstart-execution-plan` to begin enforcement
292
+ **Note**: After creating a plan, you MUST call `xpstart_execution_plan` to begin enforcement
247
293
 
248
294
  **How to use**:
249
295
  - Pass an array of task objects (NOT strings)
@@ -269,7 +315,7 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
269
315
 
270
316
  ---
271
317
 
272
- #### **2. xpget-agent-plan** - View Current Plan
318
+ #### **2. xpget_agent_plan** - View Current Plan
273
319
  **When to use**: Before deciding what to do next; to check progress
274
320
 
275
321
  **Returns**:
@@ -280,25 +326,42 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
280
326
 
281
327
  ---
282
328
 
283
- #### **3. xpcomplete-agent-plan-item** - Mark Task Complete
284
- **When to use**: **IMMEDIATELY** after finishing each task (NOT before!)
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!)
285
331
 
286
332
  **How to use**:
287
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)
288
342
  {
289
343
  "body_params": {
290
- "id": "task-uuid-from-plan"
344
+ "ids": ["35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92", "8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c", "f2b9d1c7-3e8a-4b6f-9d2c-5a7e1f4b8c3d"]
291
345
  }
292
346
  }
293
347
  ```
294
- **CRITICAL**:
295
- - Only mark complete AFTER work is actually done
296
- - This is MANDATORY for progress tracking
297
- - Get the task ID from `xpget-agent-plan` results
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
298
361
 
299
362
  ---
300
363
 
301
- #### **4. xpadd-new-agent-plan-item** - Add Discovered Task
364
+ #### **4. xpadd_new_agent_plan_item** - Add Discovered Task
302
365
  **When to use**: When you discover additional work needed during execution
303
366
 
304
367
  **How to use**:
@@ -312,7 +375,7 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
312
375
  ```
313
376
  ---
314
377
 
315
- #### **5. xpupdate-agent-plan-item** - Modify Existing Task
378
+ #### **5. xpupdate_agent_plan_item** - Modify Existing Task
316
379
  **When to use**: When task details change or need clarification
317
380
 
318
381
  **How to use**:
@@ -329,7 +392,7 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
329
392
 
330
393
  ---
331
394
 
332
- #### **6. xpdelete-agent-plan-item** - Remove Task
395
+ #### **6. xpdelete_agent_plan_item** - Remove Task
333
396
  **When to use**: When a task becomes unnecessary or redundant
334
397
 
335
398
  **How to use**:
@@ -342,8 +405,8 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
342
405
  ```
343
406
  ---
344
407
 
345
- #### **7. xpstart-execution-plan** - Start Plan Execution
346
- **When to use**: Immediately after creating a plan with `xpcreate-agent-plan`
408
+ #### **7. xpstart_execution_plan** - Start Plan Execution
409
+ **When to use**: Immediately after creating a plan with `xpcreate_agent_plan`
347
410
  **CRITICAL**: Must be called to enable enforcement mode before executing tasks
348
411
 
349
412
  **How to use**:
@@ -361,9 +424,19 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
361
424
 
362
425
  ---
363
426
 
364
- #### **8. xpask-for-information** - Ask User a Question
365
- **When to use**: When you need information from the user during plan execution
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
+
366
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.
367
440
 
368
441
  **How to use**:
369
442
  ```json
@@ -375,10 +448,13 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
375
448
  ```
376
449
 
377
450
  **What it does**:
378
- - Sets `question_raised` flag to true
379
- - Prints the question for the user
380
- - Keeps enforcement active (does NOT pause execution)
381
- - Returns waiting status
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!
382
458
 
383
459
  ---
384
460
 
@@ -386,19 +462,32 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
386
462
 
387
463
  ✅ **DO:**
388
464
  - Create comprehensive plans with ALL necessary steps
389
- - **START** the plan with `xpstart-execution-plan` after creating it
465
+ - **START** the plan with `xpstart_execution_plan` after creating it
390
466
  - Use descriptive, actionable task titles
391
467
  - Check plan before each action to stay oriented
392
- - Mark tasks complete immediately after finishing them
393
- - Ask for user information when needed with `xpask-for-information`
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**
394
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
395
475
 
396
- ❌ **DON'T:**
476
+ ❌ **DON'T - THESE ARE FORBIDDEN:**
397
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!)
398
484
  - Pass plain string arrays - must be objects with `title` field
399
485
  - Call plan tools in parallel with each other
400
486
  - Skip checking the plan between major steps
401
- - Forget to mark completed tasks
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.
402
491
 
403
492
  ---
404
493
 
@@ -407,7 +496,7 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
407
496
  ```
408
497
  1. User: "Build a REST API for user management"
409
498
 
410
- 2. Call: xpcreate-agent-plan
499
+ 2. Call: xpcreate_agent_plan
411
500
  tasks: [
412
501
  {"title": "Design user schema"},
413
502
  {"title": "Create database migration"},
@@ -416,27 +505,58 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
416
505
  {"title": "Write integration tests"}
417
506
  ]
418
507
 
419
- 3. Call: xpstart-execution-plan
508
+ 3. Call: xpstart_execution_plan
420
509
  → Plan now started, enforcement enabled (if enforce=true)
421
510
 
422
- 4. Call: xpget-agent-plan
423
- → See: Task 1 (ID: abc-123) - Design user schema - Not complete
511
+ 4. Call: xpget_agent_plan
512
+ → See: Task 1 (ID: 35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92) - Design user schema - Not complete
424
513
 
425
- 5. [Realize need user input] Call: xpask-for-information
514
+ 5. [Realize need user input] Call: xpask_for_information
426
515
  question: "Which database should we use - PostgreSQL or MySQL?"
427
516
  → question_raised=true, waiting for response
428
517
 
429
- 6. [After user responds, do the work: Design schema]
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!
430
523
 
431
- 7. Call: xpcomplete-agent-plan-item
432
- id: "abc-123"
524
+ 8. Call: xpget_agent_plan
525
+ → See: Task 1 ✓ complete, Task 2 (ID: 8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c) - Create database migration - Not complete
433
526
 
434
- 8. Call: xpget-agent-plan
435
- → See: Task 1 ✓ complete, Task 2 next...
527
+ 9. [DO THE WORK: Create migration file]
436
528
 
437
- 9. [Continue through remaining tasks]
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
+
438
544
  ```
439
- **Remember**: The plan is your roadmap. Check it often, update it as needed, and always mark tasks complete when done!
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"!
440
560
  </important_planning_instructions>
441
561
  """
442
562
 
@@ -452,7 +572,7 @@ def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task:
452
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"
453
573
  args["additional_context"] += f" \n Current execution plan: {plan_str}"
454
574
 
455
- def _load_llm_model(agent: Agent, override: Optional[Dict[str, Any]]) -> Any:
575
+ def _load_llm_model(agent: Agent, override: Optional[Dict[str, Any]] = {}) -> Any:
456
576
  """
457
577
  Load and configure the appropriate LLM model based on the agent's provider configuration.
458
578
 
@@ -667,7 +787,7 @@ def _configure_tool_calls_compression(
667
787
  args["compression_manager"] = CompressionManager(
668
788
  compress_tool_results=True,
669
789
  compress_tool_results_limit=agent.agno_settings.tool_calls_compression.threshold,
670
- 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 ""),
671
791
  )
672
792
 
673
793
  def _configure_agentic_memory(
@@ -682,7 +802,7 @@ def _configure_agentic_memory(
682
802
  args["memory_manager"] = MemoryManager(delete_memories=True,clear_memories=True)
683
803
  args["enable_agentic_memory"] = agent.agno_settings.agentic_memory
684
804
 
685
- if agent_memories_enabled:
805
+ if agent_memories_enabled and not agent.is_a_team:
686
806
  args["add_culture_to_context"] = True
687
807
 
688
808
  if agent.agno_settings.agentic_culture:
@@ -773,7 +893,7 @@ def _configure_pre_hooks(args: Dict[str, Any], agent: Agent, model: Any) -> None
773
893
  args["pre_hooks"].append(openai_moderation_guardrail)
774
894
 
775
895
 
776
- 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]:
777
897
  mcp_servers = agent.mcp_servers
778
898
 
779
899
  # combine task mcps and agent mcps
@@ -837,7 +957,9 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
837
957
  if not task.input.user or not task.input.user.id:
838
958
  raise ValueError("MCP server with OAuth authentication detected but user id not set on the task (task.input.user.id)")
839
959
 
840
- 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}")
841
963
  if not auth_result:
842
964
  raise ValueError("MCP Server authentication failed")
843
965
  if auth_result.type != MCPOAuthResponseType.TOKEN_READY:
@@ -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
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"]