@@ -526,13 +526,213 @@ interface Action {
526526| @mentions | ` entities.onTagSearch ` | ` ThreadItemConverter ` | Reference entities |
527527| Mode switch | ` composer.tools ` | Route by tool_choice | Different agents |
528528
529+ ## Critical Implementation Details
530+
531+ ### Action Object Structure
532+
533+ ** IMPORTANT** : The ` Action ` object uses ` payload ` , NOT ` arguments ` :
534+
535+ ``` python
536+ # ❌ WRONG - Will cause AttributeError
537+ action.arguments # 'Action' object has no attribute 'arguments'
538+
539+ # ✅ CORRECT
540+ action.payload # Access action data via .payload
541+ ```
542+
543+ ** Action Type Definition** :
544+ ``` python
545+ from chatkit.types import Action
546+
547+ # Action[str, Any] has these fields:
548+ action.type # str - action identifier (e.g., "task.start")
549+ action.payload # dict[str, Any] - action data
550+ action.handler # "client" | "server" - where action is processed
551+ ```
552+
553+ ### Server Action Handler Signature
554+
555+ ** CRITICAL** : The ` context ` parameter is ` RequestContext ` , NOT ` dict[str, Any] `
556+
557+ ``` python
558+ # Type annotation vs runtime reality mismatch
559+ async def action (
560+ self ,
561+ thread : ThreadMetadata,
562+ action : Action[str , Any],
563+ sender : WidgetItem | None ,
564+ context : dict[str , Any], # ⚠️ Type hint says dict, but runtime is RequestContext!
565+ ) -> AsyncIterator[ThreadStreamEvent]:
566+
567+ # ❌ WRONG - Tries to wrap RequestContext inside RequestContext
568+ request_context = RequestContext(metadata = context)
569+
570+ # ✅ CORRECT - Use context directly, it's already RequestContext
571+ user_id = context.user_id
572+ metadata = context.metadata
573+ ```
574+
575+ ** Why this happens** : ChatKit SDK passes ` RequestContext ` object at runtime, despite type annotations suggesting ` dict ` . Always use ` context ` directly without wrapping.
576+
577+ ### UserMessageItem Required Fields
578+
579+ When creating synthetic user messages from actions, ** ALL** these fields are required:
580+
581+ ``` python
582+ from chatkit.types import UserMessageItem, UserMessageTextContent
583+ from datetime import datetime
584+
585+ # ❌ WRONG - Missing required fields causes ValidationError
586+ synthetic_message = UserMessageItem(
587+ content = [UserMessageTextContent(type = " text" , text = message_text)]
588+ )
589+
590+ # ✅ CORRECT - Include all required fields
591+ synthetic_message = UserMessageItem(
592+ id = self .store.generate_item_id(" message" , thread, context),
593+ thread_id = thread.id,
594+ created_at = datetime.now(),
595+ content = [UserMessageTextContent(type = " input_text" , text = message_text)],
596+ inference_options = {},
597+ )
598+ ```
599+
600+ ** Required fields** :
601+ - ` id ` : Generate via ` store.generate_item_id("message", thread, context) `
602+ - ` thread_id ` : From ` thread.id ` parameter
603+ - ` created_at ` : Current timestamp via ` datetime.now() `
604+ - ` content ` : List of content blocks (UserMessageTextContent)
605+ - ` inference_options ` : Empty dict ` {} ` if no special options
606+
607+ ** UserMessageTextContent type values** :
608+ - ✅ ` type="input_text" ` - User text input (correct)
609+ - ❌ ` type="text" ` - Invalid for UserMessageTextContent (causes ValidationError)
610+
611+ ### Local Tool Wrappers for Widget Streaming
612+
613+ ** Problem** : Agent calls MCP tool successfully, but widget doesn't appear in UI.
614+
615+ ** Root Cause** : Widgets stream via ` RunHooks ` pattern. MCP tools alone don't trigger widget rendering - you need ** local tool wrappers** .
616+
617+ ** Solution Pattern** :
618+
619+ ``` python
620+ # 1. Create local tool wrapper
621+ from agents import function_tool
622+
623+ @function_tool
624+ async def show_task_form (
625+ ctx : RunContextWrapper[TaskFlowAgentContext],
626+ ) -> str :
627+ """ Show interactive task creation form widget."""
628+
629+ agent_ctx = ctx.context
630+ mcp_url = agent_ctx.mcp_server_url
631+
632+ # Call MCP tool via HTTP
633+ result = await _call_mcp_tool(
634+ mcp_url,
635+ " taskflow_show_task_form" ,
636+ arguments = {" params" : {" user_id" : agent_ctx.user_id}},
637+ access_token = agent_ctx.access_token,
638+ )
639+
640+ # Return result - RunHooks will intercept and stream widget
641+ return json.dumps(result)
642+
643+ # 2. Register local wrapper with agent
644+ agent = Agent(
645+ name = " TaskFlow Assistant" ,
646+ tools = [
647+ show_task_form, # Local wrapper - triggers RunHooks
648+ # ... other local wrappers
649+ ],
650+ )
651+
652+ # 3. In RunHooks.on_tool_end() - Stream widget
653+ async def on_tool_end (self , output : str | None , tool_name : str ) -> None :
654+ if tool_name == " show_task_form" :
655+ result = json.loads(output)
656+ if result.get(" action" ) == " show_form" :
657+ widget = build_task_form_widget()
658+ yield WidgetItem(... )
659+ ```
660+
661+ ** Key insight** : Direct MCP tools → no widgets. Local wrappers → RunHooks → widgets streamed.
662+
663+ ## Common Pydantic Validation Errors
664+
665+ ### Error 1: 'Action' object has no attribute 'arguments'
666+
667+ ```
668+ AttributeError: 'Action[str, Any]' object has no attribute 'arguments'
669+ ```
670+
671+ ** Fix** : Use ` action.payload ` instead of ` action.arguments `
672+
673+ ### Error 2: UserMessageTextContent type mismatch
674+
675+ ```
676+ ValidationError: Input should be 'input_text' [type=literal_error, input_value='text']
677+ ```
678+
679+ ** Fix** : Use ` type="input_text" ` for user input, not ` type="text" `
680+
681+ ### Error 3: UserMessageItem missing required fields
682+
683+ ```
684+ 4 validation errors for UserMessageItem
685+ - id: Field required
686+ - thread_id: Field required
687+ - created_at: Field required
688+ - inference_options: Field required
689+ ```
690+
691+ ** Fix** : Include all required fields when creating UserMessageItem (see pattern above)
692+
693+ ### Error 4: RequestContext wrapping issue
694+
695+ ```
696+ 2 validation errors for RequestContext
697+ user_id: Field required
698+ metadata: Input should be a valid dictionary [input_value=RequestContext(...)]
699+ ```
700+
701+ ** Fix** : Don't wrap ` context ` - it's already a RequestContext object
702+
703+ ## Widget Action Testing Checklist
704+
705+ Before claiming widget actions are complete, test:
706+
707+ - [ ] Widget renders with correct data
708+ - [ ] All buttons have clear labels (not just icons)
709+ - [ ] Client actions navigate/update UI correctly
710+ - [ ] Server actions call backend successfully
711+ - [ ] Action payload contains all required data
712+ - [ ] Widget updates after server action completes
713+ - [ ] No AttributeError on action.payload access
714+ - [ ] No ValidationError on UserMessageItem creation
715+ - [ ] Local tool wrappers trigger widget streaming
716+ - [ ] All status transitions have appropriate buttons
717+ - [ ] Test with real user session (not mock data)
718+ - [ ] Check browser console for errors
719+ - [ ] Verify backend logs show action processing
720+ - [ ] Test error cases (network failure, invalid data)
721+
529722## Anti-Patterns to Avoid
530723
5317241 . ** Mixing handlers** - Don't handle same action in both client and server
5327252 . ** Missing payload** - Always include necessary data in action payload
5337263 . ** Forgetting widget ID** - ` sendCustomAction ` requires widget ID for updates
5347274 . ** Not updating widget** - Server actions should yield ` ThreadItemReplacedEvent `
5357285 . ** Blocking in onAction** - Keep client handlers fast, offload to server
729+ 6 . ** Using action.arguments** - Use ` action.payload ` (arguments doesn't exist)
730+ 7 . ** Wrapping RequestContext** - Context is already RequestContext, don't wrap it
731+ 8 . ** Missing UserMessageItem fields** - Include id, thread_id, created_at, inference_options
732+ 9 . ** Wrong content type** - Use ` type="input_text" ` for user messages
733+ 10 . ** No local tool wrappers** - MCP tools alone don't stream widgets
734+ 11 . ** Not testing thoroughly** - Test all actions with real data before claiming done
735+ 12 . ** Assuming type hints are correct** - ChatKit has type annotation vs runtime mismatches
536736
537737## References
538738
0 commit comments