diff --git a/agentex/docs/docs/agent_types/agent_type_migration_guide.md b/agentex/docs/docs/agent_types/agent_type_migration_guide.md
new file mode 100644
index 00000000..0bc1f99d
--- /dev/null
+++ b/agentex/docs/docs/agent_types/agent_type_migration_guide.md
@@ -0,0 +1,135 @@
+# Migration Guide: Converting Between ACP Types
+
+This guide provides comprehensive, step-by-step instructions for migrating agents between complexity levels as your requirements evolve. Each migration path includes real examples, common challenges, and testing strategies.
+
+## Migration Paths Overview
+
+| From | To | When to Migrate | Complexity Increase |
+|------|----|-----------------|--------------------|
+| **Sync** | **Agentic (Base)** | Need to change from a synchronous blocking agent to a session-based asynchronous agent. | Small |
+| **Agentic (Base)** | **Agentic (Temporal)** | Need to build a long-running agent
• Handle complex multi-step / transactional tools
• Easier state management with just class variables
• Better race condition handling
• Better batch processing | Medium |
+
+## Part 1: Sync ACP → Agentic (Base) ACP
+
+Use `agentex init` to create a new project directory (select "Agentic (Base)" when prompted for ACP type), then migrate your code from the single `@acp.on_message_send` handler to the three Agentic (Base) handlers:
+
+- `@acp.on_task_create` - Initialize state or send welcome messages
+- `@acp.on_task_event_send` - Your main message processing logic (migrated from `on_message_send`)
+- `@acp.on_task_cancel` - Cleanup when tasks end
+
+**Key change:** You must manually create messages using `adk.messages.create()` instead of returning them.
+
+### Before (Sync)
+
+```python
+@acp.on_message_send
+async def handle_message_send(params: SendMessageParams):
+ # Your logic here
+ response = process_user_input(params.content.content)
+ return TextContent(author=MessageAuthor.AGENT, content=response)
+```
+
+### After (Agentic Base)
+
+```python
+@acp.on_task_create
+async def handle_task_create(params: CreateTaskParams):
+ # Optional: initialize state, send welcome message
+ pass
+
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Your logic here (migrated from on_message_send)
+ response = process_user_input(params.event.content.content)
+
+ # Manually create message (new requirement)
+ await adk.messages.create(
+ task_id=params.task.id,
+ content=TextContent(author=MessageAuthor.AGENT, content=response)
+ )
+
+@acp.on_task_cancel
+async def handle_task_cancel(params: CancelTaskParams):
+ # Optional: cleanup resources
+ pass
+```
+
+## Part 2: Agentic (Base) ACP → Agentic (Temporal) ACP
+
+Use `agentex init` to create a new project directory (select "Agentic (Temporal)" when prompted for ACP type), then migrate your three handlers into a Temporal workflow:
+
+- `@acp.on_task_create` → `@workflow.run` (workflow initialization)
+- `@acp.on_task_event_send` → `@workflow.signal` (event processing)
+- `@acp.on_task_cancel` → Handled automatically by Temporal
+
+**Key changes:**
+- State becomes class variables on the workflow
+- Wrap API calls and side effects in Temporal activities for automatic retries
+- Workflow state persists across server restarts
+
+### Before (Agentic Base)
+
+```python
+@acp.on_task_create
+async def handle_task_create(params: CreateTaskParams):
+ await adk.state.create(...)
+
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Direct API calls
+ result = await external_api.call(params.event.content)
+ await adk.messages.create(task_id=params.task.id, content=result)
+
+@acp.on_task_cancel
+async def handle_task_cancel(params: CancelTaskParams):
+ # Cleanup
+ pass
+```
+
+### After (Agentic Temporal)
+
+```python
+@workflow.defn
+class MyAgentWorkflow(BaseWorkflow):
+ def __init__(self):
+ super().__init__()
+ self.state = {} # State as class variables
+
+ @workflow.run
+ async def on_task_create(self, params: CreateTaskParams):
+ # Initialization logic
+ self.state["initialized"] = True
+
+ # Wait for events
+ await workflow.wait_condition(lambda: self._complete_task)
+
+ @workflow.signal(name=SignalName.RECEIVE_EVENT)
+ async def on_task_event_send(self, params: SendEventParams):
+ # Wrap API calls in activities (automatic retries)
+ result = await workflow.execute_activity(
+ external_api_activity,
+ params.event.content,
+ start_to_close_timeout=timedelta(minutes=5)
+ )
+
+ await workflow.execute_activity(
+ create_message_activity,
+ CreateMessageArgs(task_id=params.task.id, content=result),
+ start_to_close_timeout=timedelta(seconds=30)
+ )
+
+# Define activities for side effects
+@activity.defn
+async def external_api_activity(content):
+ return await external_api.call(content)
+
+@activity.defn
+async def create_message_activity(args):
+ await adk.messages.create(task_id=args.task_id, content=args.content)
+```
+
+---
+
+## Summary
+
+Migration between ACP types follows clear patterns. Test your migrated agent with the same inputs to ensure equivalent behavior before deploying to production.
\ No newline at end of file
diff --git a/agentex/docs/docs/agent_types/agentic/base.md b/agentex/docs/docs/agent_types/agentic/base.md
new file mode 100644
index 00000000..3af1a7d1
--- /dev/null
+++ b/agentex/docs/docs/agent_types/agentic/base.md
@@ -0,0 +1,106 @@
+# Base Agentic ACP
+
+**Base Agentic ACP** is the foundational asynchronous model for Agentex. It gives you full control over the task lifecycle with three handlers while Agentex takes care of transport, streaming, and message delivery.
+
+## Core Characteristics
+
+- **Three handler methods**
+ - `@acp.on_task_create` – initialize state or send a welcome message
+ - `@acp.on_task_event_send` – process each incoming event/message
+ - `@acp.on_task_cancel` – cleanup when a task is cancelled
+- **Explicit message creation** – create all messages via `adk.messages.create()`
+- **Asynchronous and concurrent** – multiple requests can be in-flight; your code should be async-safe
+- **State management available** – use `adk.state` when you need persistence across events
+- **No durability guarantees** – crashes and retries are your responsibility (see Temporal for durability)
+
+## Message Flow
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Agentex
+ participant Agent
+
+ Client->>Agentex: Create Task
+ Agentex->>Agent: on_task_create(params)
+ Agent->>Agentex: adk.messages.create(...)
+
+ Client->>Agentex: Send Event
+ Agentex->>Agent: on_task_event_send(params)
+ Agent->>Agent: Process logic / update state
+ Agent->>Agentex: adk.messages.create(...)
+
+ Client->>Agentex: Cancel Task
+ Agentex->>Agent: on_task_cancel(params)
+```
+
+## Basic Implementation
+
+```python
+from agentex.lib.sdk.fastacp.fastacp import FastACP
+from agentex.lib.types.fastacp import AgenticACPConfig
+from agentex.lib.types.acp import CreateTaskParams, SendEventParams, CancelTaskParams
+from agentex.lib import adk
+
+# Create Base Agentic ACP server
+acp = FastACP.create(
+ acp_type="agentic",
+ config=AgenticACPConfig(type="base")
+)
+
+@acp.on_task_create
+async def handle_task_create(params: CreateTaskParams) -> None:
+ # Optionally initialize state, then send a message
+ await adk.messages.create(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ content="Welcome!"
+ )
+
+@acp.on_task_event_send
+async def handle_task_event_send(params: SendEventParams) -> None:
+ # Process the incoming event and respond
+ await adk.messages.create(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ content=f"You said: {params.event.content}"
+ )
+
+@acp.on_task_cancel
+async def handle_task_cancel(params: CancelTaskParams) -> None:
+ # Cleanup or finalization logic
+ pass
+```
+
+## Handler Parameters
+
+### CreateTaskParams
+
+Used in `@acp.on_task_create` for task initialization:
+
+::: agentex.lib.types.acp.CreateTaskParams
+ options:
+ heading_level: 4
+ show_root_heading: false
+ show_source: false
+
+### SendEventParams
+
+Used in `@acp.on_task_event_send` for processing events:
+
+::: agentex.lib.types.acp.SendEventParams
+ options:
+ heading_level: 4
+ show_root_heading: false
+ show_source: false
+
+### CancelTaskParams
+
+Used in `@acp.on_task_cancel` for cleanup:
+
+::: agentex.lib.types.acp.CancelTaskParams
+ options:
+ heading_level: 4
+ show_root_heading: false
+ show_source: false
+
diff --git a/agentex/docs/docs/agent_types/agentic/overview.md b/agentex/docs/docs/agent_types/agentic/overview.md
new file mode 100644
index 00000000..a488f99f
--- /dev/null
+++ b/agentex/docs/docs/agent_types/agentic/overview.md
@@ -0,0 +1,90 @@
+# Agentic ACP
+
+**Agentic ACP** is the powerful, event-driven approach for complex agent interactions. It provides complete control over task lifecycle, state management, and workflows through three distinct handlers: [`on_task_create`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/lib/sdk/fastacp/base/base_acp_server.py#L314), [`on_task_event_send`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/lib/sdk/fastacp/base/base_acp_server.py#L321), [`on_task_cancel`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/lib/sdk/fastacp/base/base_acp_server.py#L339).
+
+## Core Architecture
+
+Both Base and Temporal Agentic ACP share the same three-handler pattern:
+
+```python
+@acp.on_task_create
+async def handle_task_create(params: CreateTaskParams):
+ """Initialize new tasks - setup state, send welcome messages"""
+ pass
+
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ """Process events during task lifetime - core business logic"""
+ pass
+
+@acp.on_task_cancel
+async def handle_task_cancel(params: CancelTaskParams):
+ """Clean up when tasks end - archive data, release resources"""
+ pass
+```
+
+## Task Lifecycle
+
+```mermaid
+graph LR
+ subgraph CA["Client Actions"]
+ direction TB
+ A[Client Creates Task]
+ E[Client Subscribes to Task]
+ K[Client Cancels Task]
+ F[Client Sends Event]
+ end
+
+ subgraph AH["Agent Handlers"]
+ direction TB
+ B[on_task_create]
+ G[on_task_event_send]
+ L[on_task_cancel]
+ end
+
+ subgraph AA["Agent Activity"]
+ direction TB
+ C[Initialize State]
+ D[Task Active]
+ H[Process Event]
+ I[Send Response Messages]
+ M[Cleanup Resources]
+ N[Task Complete]
+ end
+
+ %% lock column spacing
+ CA ~~~ AH
+ AH ~~~ AA
+
+ A --> B
+ B --> C
+ C --> D
+
+ F --> G
+ G --> H
+ H --> I
+ I --> E
+
+ K --> L
+ L --> M
+ M --> N
+
+ %% force a curved back-edge from J to E and keep columns
+ linkStyle 9 interpolate basis,stroke-dasharray:4 4
+```
+
+## Asynchronous Event Processing
+
+Think of Agentic ACP like a **postal system for agents** - each agent has its own mailbox where events are delivered asynchronously, and agents decide when and how to process their mail.
+
+### Every Agent Has a Mailbox
+
+Each agent has a **mailbox** where events are delivered and queued:
+
+ - Events arrive from other agents, external clients, scheduled tasks, or webhooks
+ - Events pile up whether the agent is ready or not
+ - Agents decide when to process accumulated events
+
+### Handling Concurrent Events
+
+Unlike Sync ACP, Agentic ACP doesn't lock or block past requests. If you expect to receive many concurrent requests, you'll need to handle async events yourself. You can use the Agent Task Tracker with Base ACP or asyncio Queues with Temporal to manage event processing. For more information, see [Events vs Messages](../../concepts/callouts/events_vs_messages.md).
diff --git a/agentex/docs/docs/agent_types/agentic/temporal.md b/agentex/docs/docs/agent_types/agentic/temporal.md
new file mode 100644
index 00000000..a2197e9c
--- /dev/null
+++ b/agentex/docs/docs/agent_types/agentic/temporal.md
@@ -0,0 +1,177 @@
+# Temporal Agentic ACP
+
+**Temporal Agentic ACP** provides production-ready agent development with **durable execution**, **fault tolerance**, and **automatic state management**. You don't write the ACP handlers yourself - instead, you implement Temporal workflow methods and Agentex automatically handles the ACP mapping for you.
+
+## Core Characteristics
+
+- **No explicit ACP handlers** - You implement workflow methods instead:
+ - `@workflow.run` (replaces `@acp.on_task_create`)
+ - `@workflow.signal` (replaces `@acp.on_task_event_send`)
+ - Task cancellation automatically handled (replaces `@acp.on_task_cancel`)
+- **Automatic ACP mapping** - Agentex handles the mapping for you, no manual configuration needed
+- **Durable execution** - Workflow state persisted automatically with event history
+- **Fault tolerance** - Built-in automatic retries and recovery
+- **Production-ready** - Designed for enterprise-grade reliability
+
+## Message Flow
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Agentex
+ participant TemporalACP
+ participant Workflow
+
+ Client->>Agentex: Create Task
+ Agentex->>TemporalACP: on_task_create
+ TemporalACP->>Workflow: @workflow.run(params)
+ Workflow->>Workflow: Initialize State
+ Workflow-->>TemporalACP: Workflow Running
+ TemporalACP-->>Agentex: Task Created
+ Agentex-->>Client: Task ID
+
+ Client->>Agentex: Send Event
+ Agentex->>TemporalACP: on_task_event_send
+ TemporalACP->>Workflow: @workflow.signal(params)
+ Workflow->>Workflow: Process Event
+ Workflow->>Agentex: Create Messages
+ Agentex-->>Client: Stream Response
+
+ Client->>Agentex: Cancel Task
+ Agentex->>TemporalACP: on_task_cancel
+ TemporalACP->>Workflow: Cancel Signal
+ Workflow->>Workflow: Cleanup
+ Workflow-->>TemporalACP: Workflow Complete
+```
+
+## Basic Implementation
+
+### Workflow Implementation
+
+```python
+from temporalio import workflow
+from agentex import adk
+from agentex.lib.types.acp import CreateTaskParams, SendEventParams
+from agentex.core.temporal.workflows.workflow import BaseWorkflow
+from agentex.core.temporal.types.workflow import SignalName
+from agentex.types.message_author import MessageAuthor
+from agentex.types.text_content import TextContent
+
+@workflow.defn(name="my-agent-workflow")
+class MyAgentWorkflow(BaseWorkflow):
+ def __init__(self):
+ super().__init__(display_name="My Agent")
+ self._complete_task = False
+
+ @workflow.run
+ async def on_task_create(self, params: CreateTaskParams) -> str:
+ """
+ Replaces @acp.on_task_create - Agentex maps this automatically
+ Initialize new tasks - setup state, send welcome messages
+ """
+
+ # Send initial message
+ await adk.messages.create(
+ task_id=params.task.id,
+ content=TextContent(
+ author=MessageAuthor.AGENT,
+ content="Hello! Task created."
+ ),
+ )
+
+ # Wait for task completion
+ await workflow.wait_condition(lambda: self._complete_task)
+ return "Task completed"
+
+ @workflow.signal(name=SignalName.RECEIVE_EVENT)
+ async def on_task_event_send(self, params: SendEventParams) -> None:
+ """
+ Replaces @acp.on_task_event_send - Agentex maps this automatically
+ Process events during task lifetime - core business logic
+ """
+
+ # Process event and send response
+ await adk.messages.create(
+ task_id=params.task.id,
+ content=TextContent(
+ author=MessageAuthor.AGENT,
+ content=f"You said: {params.event.content.content}"
+ ),
+ )
+
+ # Note: @acp.on_task_cancel is automatically handled by Agentex and Temporal
+ # No implementation needed
+```
+
+### ACP Configuration
+
+```python
+import os
+from agentex.lib.sdk.fastacp.fastacp import FastACP
+from agentex.lib.types.fastacp import TemporalACPConfig
+
+# Create the ACP server
+acp = FastACP.create(
+ acp_type="agentic",
+ config=TemporalACPConfig(
+ type="temporal",
+ temporal_address=os.getenv("TEMPORAL_ADDRESS", "localhost:7233")
+ )
+)
+
+# No handlers to register - Agentex automatically maps ACP to your workflow methods:
+# @acp.on_task_create → @workflow.run
+# @acp.on_task_event_send → @workflow.signal(name=SignalName.RECEIVE_EVENT)
+# @acp.on_task_cancel → Automatically handled
+```
+
+### Worker Configuration
+
+```python
+import asyncio
+from agentex.core.temporal.activities import get_all_activities
+from agentex.core.temporal.workers.worker import AgentexWorker
+from agentex.environment_variables import EnvironmentVariables
+from workflow import MyAgentWorkflow
+
+environment_variables = EnvironmentVariables.refresh()
+
+async def main():
+ task_queue_name = environment_variables.WORKFLOW_TASK_QUEUE
+ if task_queue_name is None:
+ raise ValueError("WORKFLOW_TASK_QUEUE is not set")
+
+ worker = AgentexWorker(task_queue=task_queue_name)
+
+ # get_all_activities() returns all temporal activities required for ADK
+ await worker.run(
+ activities=get_all_activities(),
+ workflow=MyAgentWorkflow,
+ )
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Workflow Parameters
+
+### CreateTaskParams
+
+Used in `@workflow.run` (replaces `@acp.on_task_create`):
+
+::: agentex.lib.types.acp.CreateTaskParams
+ options:
+ heading_level: 4
+ show_root_heading: false
+ show_source: false
+
+### SendEventParams
+
+Used in `@workflow.signal(name=SignalName.RECEIVE_EVENT)` (replaces `@acp.on_task_event_send`):
+
+::: agentex.lib.types.acp.SendEventParams
+ options:
+ heading_level: 4
+ show_root_heading: false
+ show_source: false
+
diff --git a/agentex/docs/docs/agent_types/overview.md b/agentex/docs/docs/agent_types/overview.md
new file mode 100644
index 00000000..51d189eb
--- /dev/null
+++ b/agentex/docs/docs/agent_types/overview.md
@@ -0,0 +1,34 @@
+# Agent-to-Client Protocol (ACP)
+
+The Agent-to-Client Protocol (ACP) is the foundation of how agents communicate with clients in Agentex. Understanding ACP is essential for building effective agents and choosing the right architecture for your use case. It specifies:
+
+- **Standard message formats** for agent interactions into [`TaskMessage`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/types/task_message.py) objects
+- **Decorated functions** that agents must implement as entrypoints to agent logic
+ - Sync ACP: [`on_message_send`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/lib/sdk/fastacp/base/base_acp_server.py#L346)
+ - Agentic ACP: [`on_task_create`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/lib/sdk/fastacp/base/base_acp_server.py#L314), [`on_task_event_send`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/lib/sdk/fastacp/base/base_acp_server.py#L321), [`on_task_cancel`](https://github.com/scaleapi/scale-agentex-python/blob/main/src/agentex/lib/sdk/fastacp/base/base_acp_server.py#L339)
+- **Lifecycle management** for conversations and workflows
+- **Streaming and communication** between clients and agents
+
+Think of ACP as the "language" that agents and clients use to understand each other.
+
+---
+
+## Agent Types Overview
+
+Agentex supports three agent types with different execution models and capabilities. Read the [Choose Your Agent Type](../getting_started/choose_your_agent_type.md) guide for a detailed comparison.
+
+### Quick Decision Guide
+
+**Use Sync Agents when:**
+
+- Simple request-response patterns
+- Blocking, synchronous execution is acceptable
+- Processing one request at a time is sufficient
+
+**Use Agentic Agents (Base) when:**
+
+- Asynchronous workflows and stateful applications needed
+- Must handle multiple concurrent requests
+- Need explicit control over message creation and state
+
+This is not a one-way door decision, to migrate from one ACP type to another, simply follow our [Agent Type Migration Guide](../agent_types/agent_type_migration_guide.md).
\ No newline at end of file
diff --git a/agentex/docs/docs/agent_types/sync.md b/agentex/docs/docs/agent_types/sync.md
new file mode 100644
index 00000000..e1fe12b2
--- /dev/null
+++ b/agentex/docs/docs/agent_types/sync.md
@@ -0,0 +1,68 @@
+# Sync ACP
+
+**Sync ACP** provides the simplest way to build agents in Agentex. Perfect for blocking request/response interactions (like chat bots) where you don't need complex state management or workflow control.
+
+## Core Characteristics
+
+- **Single handler method**: `@acp.on_message_send`
+- **Automatic task lifecycle management** - Tasks created and managed automatically
+- **Direct message processing** - Respond to messages immediately
+- **Automatic message management** - Agentex Server creates messages on your behalf
+- **Minimal complexity** - Focus on your business logic
+
+## Message Flow
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Agentex
+ participant Agent
+
+ Client->>Agentex: Send Message
+ Agentex->>Agentex: Auto-create Task
+ Agentex->>Agent: on_message_send(params)
+ Agent->>Agent: Process Message
+ Agent-->>Agentex: Return Response
+ Agentex-->>Client: Response
+ Note over Agentex: Task remains open for future messages
+```
+
+## Basic Implementation
+
+```python
+from agentex.lib.sdk.fastacp.fastacp import FastACP
+from agentex.lib.types.acp import SendMessageParams
+from agentex.types.text_content import TextContent
+from agentex.types.message_author import MessageAuthor
+
+# Create Sync ACP server
+acp = FastACP.create(acp_type="sync")
+
+@acp.on_message_send
+async def handle_message(params: SendMessageParams):
+ """Process incoming messages and return responses"""
+
+ # Access the user's message
+ user_message = params.content.content
+
+ # Process and create response
+ response_text = f"You said: {user_message}"
+
+ # Return response (Agentex auto-creates the message)
+ return TextContent(
+ author=MessageAuthor.AGENT,
+ content=response_text
+ )
+```
+
+## Handler Parameters
+
+### SendMessageParams
+
+The `@acp.on_message_send` handler receives:
+
+::: agentex.lib.types.acp.SendMessageParams
+ options:
+ heading_level: 4
+ show_root_heading: false
+ show_source: false
diff --git a/agentex/docs/docs/api/overview.md b/agentex/docs/docs/api/overview.md
index 998f0616..9f6ed3f5 100644
--- a/agentex/docs/docs/api/overview.md
+++ b/agentex/docs/docs/api/overview.md
@@ -165,6 +165,4 @@ message = await adk.messages.create(task_id=task_id, content=content)
state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id)
```
-## Next Steps
-
-Ready to dive into the details? Start with **[Concepts](../concepts/task.md)** to understand core concepts, then explore **[Types](types.md)** for complete API reference.
\ No newline at end of file
+
\ No newline at end of file
diff --git a/agentex/docs/docs/api/types.md b/agentex/docs/docs/api/types.md
index 0f60746b..84f3e7df 100644
--- a/agentex/docs/docs/api/types.md
+++ b/agentex/docs/docs/api/types.md
@@ -130,8 +130,4 @@ For practical usage examples and patterns, see:
- [State Concepts](../concepts/state.md) - Managing persistent state
- [Agent-to-Client Protocol (ACP)](../acp/overview.md) - Sync and Agentic ACP handler parameters explained
-## Next Steps
-
-- Explore individual [Concepts](../concepts/task.md) for detailed explanations
-- See [Advanced Concepts](../concepts/callouts/overview.md) for implementation patterns
-- Check [Tutorials](../tutorials.md) for practical examples
\ No newline at end of file
+
\ No newline at end of file
diff --git a/agentex/docs/docs/concepts/agents.md b/agentex/docs/docs/concepts/agents.md
index a11df976..80b2aac7 100644
--- a/agentex/docs/docs/concepts/agents.md
+++ b/agentex/docs/docs/concepts/agents.md
@@ -4,7 +4,7 @@ Agents are the core building blocks of Agentex applications. Understanding what
## What is an Agent?
-In Agentex, this of **an Agent as just code** - specifically, it is just Python code that adheres to the Agent-to-Client Protocol (ACP). This allows clients to speak to any agent the same way regardless of the agent's complexity. This allows agent developers full unopinionated control over which libraries they use, which models they use, and how they implement their business logic.
+In Agentex, think of **an Agent as just code** - specifically, it is just Python code that adheres to the Agent-to-Client Protocol (ACP). This allows clients to speak to any agent the same way regardless of the agent's complexity. This allows agent developers full unopinionated control over which libraries they use, which models they use, and how they implement their business logic.
An agent defines:
diff --git a/agentex/docs/docs/concepts/architecture.md b/agentex/docs/docs/concepts/architecture.md
index 0335c676..6d4d9567 100644
--- a/agentex/docs/docs/concepts/architecture.md
+++ b/agentex/docs/docs/concepts/architecture.md
@@ -69,10 +69,3 @@ To respond to client requests, agent developers only need to implement a fixed s
**👉 Read [Developing Agentex Agents](../developing_agentex_agents.md) to understand your options and choose the right approach for your use case.**
----
-
-## Next Steps
-
-- **New to Agentex?** Follow the [Quick Start Guide on GitHub](https://github.com/scaleapi/scale-agentex#quick-start)
-- **Ready to build?** Check out [Tutorials on GitHub](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials)
-- **Understand the protocol:** Read about [Agent Client Protocol (ACP)](../acp/overview.md)
diff --git a/agentex/docs/docs/concepts/events.md b/agentex/docs/docs/concepts/events.md
index a11afb8f..5c6c78aa 100644
--- a/agentex/docs/docs/concepts/events.md
+++ b/agentex/docs/docs/concepts/events.md
@@ -127,9 +127,4 @@ async def handle_with_context(params: SendEventParams):
- **Agent Creates Messages**: Agent developers create TaskMessages, not the server
- **Cursor-Based**: Use with Agent Task Tracker for progress tracking
-## Next Steps
-
-- Learn about [Agent Task Tracker](agent_task_tracker.md) for distributed coordination
-- Explore [State Management](state.md) for maintaining agent context
-- See [Critical Concepts - Events vs Messages](callouts/events_vs_messages.md) for critical distinctions
-- Review the [Tutorials](../tutorials.md) for hands-on examples
\ No newline at end of file
+
\ No newline at end of file
diff --git a/agentex/docs/docs/concepts/streaming.md b/agentex/docs/docs/concepts/streaming.md
index b589744c..8b5ff3d3 100644
--- a/agentex/docs/docs/concepts/streaming.md
+++ b/agentex/docs/docs/concepts/streaming.md
@@ -200,8 +200,4 @@ For complete type definitions, see:
::: agentex.types.text_delta.TextDelta
-## Next Steps
-
-- Learn about [State Concepts](state.md) for persistent data during streaming
-- Explore [Advanced Streaming Concepts](callouts/streaming.md) for complex streaming patterns
-- See [TaskMessage Concepts](task_message.md) for message structure
\ No newline at end of file
+
\ No newline at end of file
diff --git a/agentex/docs/docs/deployment/cicd.md b/agentex/docs/docs/deployment/cicd.md
new file mode 100644
index 00000000..4936a68b
--- /dev/null
+++ b/agentex/docs/docs/deployment/cicd.md
@@ -0,0 +1,255 @@
+# Integrate CI/CD
+
+This guide shows how to integrate Agentex into your CI/CD pipeline to automatically build, push, and deploy your agent to a Kubernetes cluster whenever code changes are pushed to your repository.
+
+---
+
+## Overview
+
+Agentex's deployment commands (`agentex agents build`, `agentex secrets sync`, `agentex agents deploy`) are designed to run in any CI/CD system—GitHub Actions, GitLab CI, Jenkins, CircleCI, Azure DevOps, Bitbucket Pipelines, or your custom automation.
+
+!!! note "Examples Use GitHub Actions"
+ The examples in this guide use **GitHub Actions** syntax, but the core Agentex commands (`agentex agents build`, `agentex secrets sync`, `agentex agents deploy`) work identically in any CI/CD platform. Only the authentication mechanisms and pipeline syntax differ between platforms—the Agentex deployment commands remain the same.
+
+### The Pipeline Flow
+
+The diagram below illustrates a typical end-to-end automation workflow. The four **core Agentex stages** are:
+
+1. **Build** - Create a Docker image of your agent with version metadata
+2. **Push** - Upload the image to your container registry (GCR, ECR, ACR, GitHub Container Registry)
+3. **Sync Secrets** - Inject credentials from your secrets manager into the Kubernetes namespace
+4. **Deploy** - Roll out the agent to Kubernetes using Helm with environment-specific configurations
+
+Each stage requires authentication (to your registry, secrets manager, and Kubernetes cluster), which you'll configure using your CI/CD platform's secrets management (environment variables, secret stores, workload identity, etc.).
+
+**Customize to your process**: These four stages are the essential Agentex deployment steps, but you can integrate them into your organization's existing release process. Common additions include testing phases (run unit tests before build, integration tests after build), security scanning (vulnerability scanning, SAST/DAST), manual approval gates for production, compliance checks, and custom notifications. The Agentex commands fit seamlessly into any CI/CD workflow structure.
+
+### Example Pipeline Architecture
+
+!!! note "Example Integration Pattern"
+ The diagram below shows **one example** of how these stages can be orchestrated. Your implementation may differ based on your CI/CD platform (GitHub Actions, GitLab CI, Jenkins, etc.), infrastructure choices (cloud provider, registry, secrets manager), and you may integrate additional steps based on your organization's requirements—testing phases, security scanning, approval workflows, or other custom validation steps. The core four-stage pattern (Build → Push → Secrets Sync → Deploy) remains consistent, but you control what happens before, between, and after these stages.
+
+```mermaid
+graph TB
+ Start([Code Push to Git Repository]) --> Trigger[CI/CD Pipeline Triggered]
+
+ Trigger --> Build[Build Stage]
+
+ subgraph Build["🔨 Build Agent Image"]
+ B1[Install agentex-sdk]
+ B2[Generate build-info.json]
+ B3[Run agentex agents build]
+ B4[Tag image with commit SHA]
+
+ B1 --> B2 --> B3 --> B4
+ end
+
+ Build --> AuthRegistry[Authenticate to Container Registry]
+ AuthRegistry --> Push[Push Stage]
+
+ subgraph Push["📦 Push to Registry"]
+ P1[Push Docker image]
+ P2[Image available at
registry/repo:tag]
+
+ P1 --> P2
+ end
+
+ Push --> SecretStage[Secrets Sync Stage]
+
+ subgraph SecretStage["🔐 Sync Secrets"]
+ S1[Authenticate to Secrets Manager
AWS Secrets Manager / Azure Key Vault / etc.]
+ S2[Fetch production secrets]
+ S3[Construct secrets YAML
credentials + imagePullSecrets]
+ S4[Authenticate to Kubernetes cluster]
+ S5[Run agentex secrets sync
--no-interactive]
+ S6[Secrets created in K8s namespace
as Kubernetes Secrets]
+
+ S1 --> S2
+ S2 --> S3
+ S3 --> S4
+ S4 --> S5
+ S5 --> S6
+ end
+
+ SecretStage --> Deploy[Deploy Stage]
+
+ subgraph Deploy["🚀 Deploy to Kubernetes"]
+ D1[Select environment
dev/staging/prod]
+ D2[Run agentex agents deploy
--no-interactive]
+ D3[Helm chart applied to cluster]
+
+ D1 --> D2
+ D2 --> D3
+ end
+
+ Deploy --> Success([✅ Deployment Complete])
+
+ Build -.->|Build fails| Failure([❌ Pipeline Failed])
+ Push -.->|Push fails| Failure
+ SecretStage -.->|Secrets sync fails| Failure
+ Deploy -.->|Deploy fails| Failure
+
+ style Start fill:#e1f5ff
+ style Success fill:#d4f4dd
+ style Failure fill:#ffe1e1
+ style Build fill:#fff4e1
+ style Push fill:#f0e1ff
+ style SecretStage fill:#ffe1f0
+ style Deploy fill:#e1ffe8
+```
+
+---
+
+## Prerequisites
+
+Before setting up the CI/CD pipeline, ensure you have:
+
+### Infrastructure
+
+- **Kubernetes cluster** - Access to a Kubernetes cluster where you'll deploy
+- **Namespace** - A namespace provisioned for your agent (e.g., `agentex-agents`)
+- **Container registry** - Docker registry to store images (GCR, ECR, ACR, or GitHub Container Registry)
+- **Secret management** - System for storing production secrets (AWS Secrets Manager, Azure Key Vault, etc.)
+
+### Permissions
+
+- **Cluster RBAC** - Permissions to create deployments, secrets, and services in your namespace
+- **Registry access** - Ability to push images to your container registry
+- **GitHub repository access** - Admin access to add workflows and secrets (if using GitHub Actions, or equivalent access for your CI/CD platform)
+
+### Local Setup (for testing)
+
+- **kubectl** - Installed and configured with access to your cluster
+- **agentex-sdk** - Installed locally for testing commands
+
+Verify your setup:
+
+```bash
+kubectl config current-context # Check cluster connection
+kubectl get namespace your-namespace # Verify namespace exists
+kubectl auth can-i create deployments -n your-namespace # Check permissions
+```
+
+---
+
+## Pipeline Overview
+
+The pipeline will need to:
+
+1. Build agent image
+2. Push this image to the registry
+3. Sync secrets to Kubernetes namespace
+4. Deploy agent image to the namespace
+
+### Building agent
+
+For this step, you will need to decide which Docker registry you will use to store the built agent images.
+
+For example, you may decide to use the Azure, Google, or AWS Container Registry.
+
+Whichever registry you choose, you will need to make sure to authenticate your CI/CD runner which executes your pipeline.
+
+As an example for GitHub Actions, if using Azure you might have a step in your job which uses the `azure/login` package and then calls `az acr login --name your-registry-here`.
+
+You may also choose to generate some build information as part of your CI/CD pipeline, like the build date or commit author.
+
+If you want to have this show up in the agent's deployment history view, you can write this info to a `build-info.json` file in the agent's `project` directory.
+
+The following fields are supported and will be parsed out into the deployment history view:
+
+```bash
+
+{
+ "agent_path": "agents/myagent",
+ "agent_repo": "https://github.com/your/repo",
+ "agent_commit": "shahash",
+ "build_time": "2025-10-25T14:30:00.000Z",
+ "author_name": "John Doe",
+ "author_email": "doe@example.com",
+ "branch_name": "main"
+}
+```
+
+Finally, you will want to make sure the agentex SDK is installed (e.g. with `pip install agentex-sdk`) and then run the build command as described in the overview:
+```bash
+agentex agents build \
+ --manifest "agents/myagent/manifest.yaml" \
+ --registry "your-registry-here" \ # e.g. ghcr.io
+ --tag "your-image-tag" \
+ --platforms "linux/amd64" \
+ --repository-name your-repository-name \
+ --push
+```
+
+This will build and push your agent image to `your-registry-here/your-repository-name:your-image-tag` where it will be available to deploy.
+
+### Syncing Secrets
+
+Agents require credentials (API keys, database URLs, etc.) that live in your secrets manager (AWS Secrets Manager, Azure Key Vault, etc.), not in code. The `agentex secrets sync` command bridges your secrets manager to Kubernetes by injecting credentials directly into the target namespace.
+
+**Authentication requirements**: Your CI/CD runner (e.g., GitHub Actions runner) needs access to both your secrets manager (to read secrets) and your Kubernetes cluster (to create Secret objects).
+
+**How it works**: The workflow fetches secrets from your secrets manager and constructs a YAML file containing the credentials and image pull secrets. This YAML is then passed to the sync command, which creates Kubernetes Secret objects in your namespace.
+
+**Secrets YAML structure:**
+
+```yaml
+credentials:
+ API_KEY_1:
+ api-key-jdoe: abc12345
+ api-key-jsmith: def6789
+ REDIS_URL_SECRET:
+ redis-url-secret: redis://localhost/
+imagePullSecrets:
+ pull-secret-1:
+ registry: registry-url
+ username: jdoe
+ password: token
+ email: doe@example.com
+```
+
+- **`credentials`** map to the `credentials` section in your manifest. Each secret name contains key-value pairs for the actual credentials.
+- **`imagePullSecrets`** map to registry credentials needed to pull private images. Include registry URL, username, and password/token.
+
+Once you've constructed the secrets YAML (typically by fetching from your secrets manager in the pipeline), sync them with:
+
+```bash
+agentex secrets sync
+ --manifest "path/to/agent/manifest.yaml" \
+ --cluster desired-cluster \
+ --namespace agent-namespace \
+ --values prod-secrets.yaml \
+ --no-interactive
+```
+
+### Deploying
+
+Once an image has been built and pushed to the container registry of your choice, and all the secrets have been synced to the Kubernetes namespace, the last remaining step is to deploy the tagged image.
+
+As in the previous section, this step will require that your CI/CD runner has access to the Kubernetes cluster where you wish to deploy this agent.
+
+Agentex supports environment based helm overrides via the `environments.yaml` file which lives in the same directory as the manifest. You can use any of the environment names specified there to include the appropriate overrides like number of replicas, resource requirements, or additional environment variables.
+
+Beyond this, you can use the same manifest, cluster, and namespace values as before when syncing secrets as well as the repository and tag values used when pushing the image in the first section.
+
+The full command to deploy will look something like this:
+
+```bash
+agentex agents deploy \
+ --environment your-env \
+ --manifest "path/to/agent/manifest.yaml" \
+ --cluster desired-cluster \
+ --namespace agent-namespace \
+ --repository your-registry.com/your-repo-name \
+ --tag image-tag-from-above \
+ --no-interactive
+```
+
+---
+
+## Next Steps
+
+- Review [Deployment Commands](commands.md) for detailed command documentation
+- Configure [Manifest Configuration](../manifest_setup.md) for your agent
+- Set up monitoring and alerting for your deployed agents
+- Create a staging environment for testing before production deployments
\ No newline at end of file
diff --git a/agentex/docs/docs/deployment/commands.md b/agentex/docs/docs/deployment/commands.md
index 9b231d4e..3bf3fc02 100644
--- a/agentex/docs/docs/deployment/commands.md
+++ b/agentex/docs/docs/deployment/commands.md
@@ -219,9 +219,3 @@ kubectl logs -f -l app.kubernetes.io/name=agentex-agent -n your-namespace
helm status my-agent -n your-namespace
```
----
-
-## Next Steps
-
-- Need configuration help? See [Manifest Configuration](../manifest_setup.md)
-- Return to [Deployment Overview](overview.md)
diff --git a/agentex/docs/docs/deployment/overview.md b/agentex/docs/docs/deployment/overview.md
index 228072cd..98e60014 100644
--- a/agentex/docs/docs/deployment/overview.md
+++ b/agentex/docs/docs/deployment/overview.md
@@ -5,6 +5,7 @@ Deploying your agent to production involves three main steps that prepare, confi
## Prerequisites
Before deploying, you need:
+
- **kubectl** installed and configured
- **Cluster access** - Contact your cluster administrators for access
- **Namespace** - Get a namespace provisioned for your agent
@@ -93,8 +94,41 @@ agentex agents deploy --environment prod --cluster prod-cluster \
--no-interactive
```
-## Next Steps
-1. Review the [Commands Reference](commands.md) for detailed command documentation
-2. Configure your agent with the [Manifest Configuration](../manifest_setup.md) guide
-3. Follow the Quick Start sequence above to deploy your agent
+## Moving to Production: CI/CD Deployment
+
+Agentex is designed from the ground up for automated, enterprise-grade deployment. Rather than treating CI/CD as an afterthought, the platform embraces a **"build once, deploy everywhere"** philosophy where the same Docker image flows through development, staging, and production environments with security and automation built in from day one.
+
+### Why CI/CD with Agentex?
+
+Manual deployments don't scale. When you're managing multiple agents across environments, copying commands between terminals and manually syncing secrets becomes error-prone and time-consuming. Agentex's deployment architecture solves this by integrating three critical capabilities:
+
+**Immutable Artifacts**: Build your agent once into a Docker image, then deploy that exact same artifact to dev, staging, and production. No "works on my machine" problems, no environment drift.
+
+**Secure Secrets Management**: Secrets never live in code. The `agentex secrets sync` command bridges your secrets manager (AWS Secrets Manager, Azure Key Vault, etc.) directly to Kubernetes, creating a secure pipeline where credentials flow from source of truth to runtime without ever touching disk or code repositories.
+
+**Environment Promotion**: Use the same manifest and commands across all environments. The only difference is which secrets YAML you load and which environment profile you select—everything else is identical.
+
+### The Automated Pipeline
+
+A typical CI/CD pipeline for Agentex follows four core stages:
+
+1. **Build** - Create a Docker image with `agentex agents build` and generate metadata (commit SHA, author, timestamp) for audit trails
+2. **Push** - Upload the image to your container registry (GCR, ECR, ACR, or GitHub Container Registry)
+3. **Secrets Sync** - Run `agentex secrets sync` to inject credentials from your secrets manager into the Kubernetes namespace
+4. **Deploy** - Execute `agentex agents deploy` with environment-specific Helm overrides to roll out the agent
+
+These four stages are the **core Agentex deployment steps**, but your organization can integrate additional phases around them based on your release processes—testing phases (unit, integration, E2E tests), security scanning, compliance checks, manual approval gates, or any custom validation steps your team requires. The Agentex commands are designed to fit seamlessly into your existing CI/CD workflows.
+
+### Getting Started with CI/CD
+
+Ready to automate your deployments? The [CI/CD Setup Guide](cicd.md) walks through:
+
+- Setting up GitHub Actions workflows for automatic deployment
+- Configuring secrets synchronization with your secrets manager
+- Implementing build metadata for deployment history tracking
+- Environment-specific deployment strategies
+
+The commands you've learned in this guide (build, secrets sync, deploy) are the same commands your CI/CD pipeline will use—just executed automatically instead of manually.
+
+
diff --git a/agentex/docs/docs/developing_agentex_agents.md b/agentex/docs/docs/developing_agentex_agents.md
index e6f3a51a..7e0863b8 100644
--- a/agentex/docs/docs/developing_agentex_agents.md
+++ b/agentex/docs/docs/developing_agentex_agents.md
@@ -118,24 +118,4 @@ When you outgrow your current approach, Agentex provides clear upgrade paths:
---
-## Next Steps
-
-### Ready to Build?
-
-1. **Choose your approach** using the decision framework above
-2. **Read the concepts** for your chosen approach:
- - [Agent-to-Client Protocol (ACP)](acp/overview.md) - Covers Sync and Agentic ACP
- - [Temporal Guide](temporal-guide.md)
-3. **Set up your environment** with the [Quick Start Guide on GitHub](https://github.com/scaleapi/scale-agentex#quick-start)
-4. **Follow the tutorials** to see it in action
-5. **Create your agent** with `agentex init`
-
-### Need Help Deciding?
-
-- **Browse tutorials** to see different approaches in action
-- **Read the concepts** to understand technical differences
-- **Check out critical concepts** for important considerations: [Critical Concepts](concepts/callouts/overview.md)
-
----
-
**Remember**: You can always start simple and upgrade later. Agentex is designed to grow with your needs.
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/agent_task_tracker.md b/agentex/docs/docs/development_guides/agent_task_tracker.md
new file mode 100644
index 00000000..3cfdfe5b
--- /dev/null
+++ b/agentex/docs/docs/development_guides/agent_task_tracker.md
@@ -0,0 +1,138 @@
+# Agent Task Tracker
+
+Agent Task Trackers are **optional** coordination objects that help track processing progress and status for agents working on tasks. They are useful for building resumable and stateful processing patterns, but are not required for basic agent functionality.
+
+## What Are Agent Task Trackers?
+
+Agent Task Trackers are **optional processing coordination records** that track the state of agent work on specific tasks. Each tracker represents the relationship between one agent and one task.
+
+**Key Characteristics:**
+
+- **Optional**: Not required for basic agent functionality
+- **Automatically Created**: Generated when a task is assigned to an agent
+- **Unique per Agent/Task**: One tracker per agent-task combination
+- **Status Management**: Tracks processing state for coordination
+- **Cursor-Based Progress**: Records processing position using `last_processed_event_id` as a cursor
+
+## Agent Task Tracker Structure
+
+```python
+class AgentTaskTracker:
+ id: str # Unique tracker identifier
+ agent_id: str # Associated agent
+ task_id: str # Associated task
+ status: Optional[str] # Processing status
+ status_reason: Optional[str] # Description of current status
+ last_processed_event_id: Optional[str] # CURSOR - tracks processing position
+ created_at: datetime # When tracker was created
+ updated_at: Optional[datetime] # Last update timestamp
+```
+
+## When to Use Agent Task Trackers
+
+Use Agent Task Trackers when you need:
+
+- **Batch Processing**: Wait for multiple events before processing together
+- **State-Based Processing**: Ignore events when agent is busy, process accumulated events when ready
+- **Resumable Processing**: Resume from specific event positions after restarts
+- **Long Workflows**: Coordinate multi-phase processing (e.g., research → analysis → response)
+
+!!! warning "Optional Feature"
+ Agent Task Trackers are **not required** for basic agent functionality. Only use them when you need stateful processing coordination.
+
+## Cursor Behavior (Critical)
+
+The `last_processed_event_id` field acts as a **CURSOR** with strict rules:
+
+!!! danger "Cursor Rules"
+ 1. **Forward-Only**: Cursors can only move forward - never backward
+ 2. **Set After Completion**: Only update cursor AFTER processing is finished
+ 3. **Update Fails on Rollback**: Attempting to set cursor to an earlier event will fail
+
+## Common Processing Patterns
+
+### Simple Batch Processing
+Wait for multiple events, then process together:
+```python
+@acp.on_task_event_send
+async def batch_handler(params: SendEventParams):
+ unprocessed_events = await get_unprocessed_events(params.task.id, params.agent.id)
+
+ if len(unprocessed_events) >= 5: # Wait for batch of 5
+ await process_batch(unprocessed_events)
+ await commit_cursor(unprocessed_events[-1].id)
+```
+
+### State-Based Processing
+Ignore events when not ready, process when ready:
+```python
+@acp.on_task_event_send
+async def state_handler(params: SendEventParams):
+ if agent_busy():
+ return # Events accumulate while agent works
+
+ # Agent ready - get accumulated events and decide strategy
+ events = await get_unprocessed_events(params.task.id, params.agent.id)
+ await start_processing_workflow(events)
+```
+
+!!! tip "Lightweight Handlers"
+ Keep event handlers fast - they should check conditions and kick off background processing, not do heavy work directly.
+
+## Basic Usage Pattern
+
+```python
+async def process_with_tracker(task_id: str, agent_id: str):
+ # Get current tracker state
+ tracker = await adk.agent_task_tracker.get_by_task_and_agent(
+ task_id=task_id,
+ agent_id=agent_id
+ )
+
+ # Get unprocessed events since last cursor
+ events = await adk.events.list_events(
+ task_id=task_id,
+ agent_id=agent_id,
+ last_processed_event_id=tracker.last_processed_event_id,
+ limit=50
+ )
+
+ if not events:
+ return # No new events to process
+
+ # Process all events in the batch
+ for event in events:
+ await process_event(event)
+
+ # Update cursor ONLY after all processing is complete
+ await adk.agent_task_tracker.update(
+ tracker_id=tracker.id,
+ request=UpdateAgentTaskTrackerRequest(
+ last_processed_event_id=events[-1].id,
+ status_reason=f"Processed {len(events)} events"
+ )
+ )
+```
+
+## Querying Agent Task Trackers
+
+```python
+# Get by tracker ID
+tracker = await adk.agent_task_tracker.get(tracker_id="tracker_123")
+
+# Get by agent and task (most common pattern)
+tracker = await adk.agent_task_tracker.get_by_task_and_agent(
+ agent_id="agent_456",
+ task_id="task_789"
+)
+```
+
+## Key Points
+
+- **Optional Feature**: Agent Task Trackers are not required for basic functionality
+- **Cursor-Only Updates**: Only update `last_processed_event_id` after processing completion
+- **Forward Movement**: Cursors cannot move backward - updates that try to rollback will fail
+- **Event Accumulation**: Events naturally accumulate when agents ignore them during busy periods
+- **Lightweight Handlers**: Event handlers should be fast - heavy processing happens asynchronously
+
+
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/events_vs_messages.md b/agentex/docs/docs/development_guides/events_vs_messages.md
new file mode 100644
index 00000000..444dc94f
--- /dev/null
+++ b/agentex/docs/docs/development_guides/events_vs_messages.md
@@ -0,0 +1,229 @@
+# Events vs Messages
+
+!!! danger "Critical for Agentic ACP"
+ **Events and TaskMessages serve different purposes and are stored in separate database tables.** This distinction is fundamental to understanding how Agentic ACP works.
+
+## The Core Distinction
+
+| **Events** | **TaskMessages** |
+|------------|------------------|
+| ✅ **Stored persistently** in events table | ✅ **Stored persistently** in messages table |
+| 🔄 **Agent processing notifications** | 💬 **User-facing conversation history** |
+| ✅ **Can be queried** for processing | ✅ **Can be retrieved** for conversation context |
+| 🚀 **Written to DB BEFORE ACP delivery** | 📝 **Created by agents or users directly** |
+
+## Understanding the Relationship
+
+### Events Are "Processing Queue" Items
+
+Think of events like **work items** in a processing queue:
+
+```python
+# Event = "Work item ready for processing"
+# - Stored persistently in events table
+# - Contains processing context and metadata
+# - Written to database BEFORE being sent to agent
+# - Can be queried and tracked for processing progress
+
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # This event was already saved to DB before reaching here
+ event = params.event # ✅ Persistent in events table
+
+ # You can query all events for this task/agent
+ all_events = await adk.events.list_events(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ ) # ✅ All processing history available
+
+ # TaskMessages are the user-facing conversation
+ messages = await adk.messages.list(task_id=params.task.id) # ✅ Conversation context
+```
+
+### TaskMessages Are the "User Conversation"
+
+```python
+# TaskMessage = User-facing conversation history
+# - Permanently stored in messages table
+# - Contains conversation content for users/clients
+# - Can be retrieved anytime for chat history
+# - Forms the visible conversation thread
+
+# Access user-facing conversation history
+task_messages = await adk.messages.list(task_id=task_id)
+for message in task_messages:
+ print(f"User sees: {message.content}")
+
+# Events are for agent processing coordination
+events = await adk.events.list_events(task_id=task_id, agent_id=agent_id)
+for event in events:
+ print(f"Agent processes: {event.id} at sequence {event.sequence_id}")
+```
+
+## Event Processing Patterns
+
+### ❌ Don't Process Only Current Event Content
+
+```python
+# WRONG: Only processing the single event content
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ if params.event.content:
+ # Only processing the current event content
+ user_message = params.event.content.content
+ response = await process_message(user_message)
+ # Missing: No context from conversation history or other events!
+```
+
+### ✅ Use Events for Coordination, Process with Full Context
+
+```python
+# CORRECT: Event triggers processing, get full context from both sources
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Event tells us "work is ready to be processed"
+ # (This event is already stored in DB before reaching here)
+ event = params.event
+
+ # Get conversation context from TaskMessages
+ conversation_messages = await adk.messages.list(task_id=params.task.id)
+
+ # Get processing context from Events (for coordination)
+ all_events = await adk.events.list_events(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+
+ # Process with full context from both sources
+ response = await process_with_context(conversation_messages, all_events)
+
+ # Create new message for user conversation
+ await adk.messages.create(
+ task_id=params.task.id,
+ content=TextContent(
+ author=MessageAuthor.AGENT,
+ content=response
+ )
+ )
+```
+
+## Why This Architecture Exists
+
+### Separation of Concerns: Processing vs Conversation
+
+The dual-table architecture enables powerful patterns:
+
+1. **Events Table**: Tracks agent processing state and coordination
+2. **Messages Table**: Maintains user-facing conversation history
+
+### Enables Flexible Processing Strategies
+
+```python
+# Strategy 1: Process events immediately
+@acp.on_task_event_send
+async def immediate_processing(params: SendEventParams):
+ # React to each event as it arrives
+ # Events are already in DB for coordination
+ response = await quick_response(params.event.content)
+
+# Strategy 2: Batch process accumulated events
+@acp.on_task_event_send
+async def batch_processing(params: SendEventParams):
+ # Get current processing cursor from agent task tracker
+ tracker = await adk.agent_task_tracker.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+
+ # Get all unprocessed events since last cursor
+ unprocessed_events = await adk.events.list_events(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ last_processed_event_id=tracker.last_processed_event_id,
+ limit=100
+ )
+
+ if len(unprocessed_events) >= 5: # Process batch of 5
+ await process_event_batch(unprocessed_events)
+
+ # Update cursor to track progress - ONLY after processing is complete
+ await adk.agent_task_tracker.update(
+ tracker_id=tracker.id,
+ request=UpdateAgentTaskTrackerRequest(
+ last_processed_event_id=unprocessed_events[-1].id,
+ status_reason=f"Processed batch of {len(unprocessed_events)} events"
+ )
+ )
+```
+
+### Agent Task Tracker for Cursor Coordination
+
+**Agent Task Tracker** provides cursor-based coordination for sophisticated event processing patterns:
+
+```python
+@acp.on_task_event_send
+async def cursor_coordinated_processing(params: SendEventParams):
+ # Agent Task Tracker acts as processing cursor
+ tracker = await adk.agent_task_tracker.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+
+ # Get unprocessed events since last cursor position
+ unprocessed_events = await adk.events.list_events(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ last_processed_event_id=tracker.last_processed_event_id,
+ limit=50
+ )
+
+ if not unprocessed_events:
+ return # No new events to process
+
+ # Process events AND get conversation context from TaskMessages
+ conversation_messages = await adk.messages.list(task_id=params.task.id)
+
+ # Process with full context from both events and messages
+ for event in unprocessed_events:
+ await process_with_full_context(event, conversation_messages)
+
+ # Update cursor - forward-only movement after successful processing
+ await adk.agent_task_tracker.update(
+ tracker_id=tracker.id,
+ request=UpdateAgentTaskTrackerRequest(
+ last_processed_event_id=unprocessed_events[-1].id,
+ status_reason=f"Processed {len(unprocessed_events)} events with conversation context"
+ )
+ )
+```
+
+**Key Benefits:**
+
+- **Resumable Processing**: Pick up where you left off after restarts
+- **Forward-Only Progress**: Cursors prevent duplicate processing
+- **Batch Coordination**: Process multiple events atomically
+- **Progress Tracking**: Track processing status across instances
+
+!!! warning "Cursor Rules"
+ - Cursors can only move **forward** - never backward
+ - Update cursor **ONLY** after processing is complete
+ - Use Agent Task Tracker cursors for **coordination** - they're optional for basic processing
+
+### Database Write Order Guarantees
+
+**Critical**: Events are written to the database **BEFORE** being sent to the agent:
+
+1. **User sends message** → TaskMessage created in messages table
+2. **Event created** → Event written to events table
+3. **Event delivered** → Agent receives event via ACP
+4. **Agent processes** → Can query both events and messages tables
+
+This ensures agents can always access the event data, even if there are delivery failures.
+
+## Key Takeaway
+
+!!! info "Remember the Architecture"
+ - **Events**: Persistent processing coordination stored in events table (written BEFORE ACP delivery)
+ - **TaskMessages**: Persistent user conversation stored in messages table
+ - **Use events for processing coordination**, **TaskMessages for conversation context**
+ - **Both are queryable and persistent** - they serve different purposes
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/jupyter_notebooks.md b/agentex/docs/docs/development_guides/jupyter_notebooks.md
new file mode 100644
index 00000000..15ffb3b2
--- /dev/null
+++ b/agentex/docs/docs/development_guides/jupyter_notebooks.md
@@ -0,0 +1,324 @@
+# Jupyter Notebooks for Agent Development
+
+Jupyter Notebooks provide an excellent environment for developing and testing AgentEx agents. Every project created with `agentex init` automatically includes a `dev.ipynb` notebook, pre-configured with your agent name and ready-to-use examples for testing your agent.
+
+!!! note "Recommended: Use Agentex UI for Local Development"
+ This documentation is primarily for **local development** using notebooks or programmatic access. For most users, we recommend using the **[Agentex UI](https://github.com/scaleapi/scale-agentex/tree/main/agentex-ui){target="_blank"}** instead, as it automatically handles streaming, delta aggregation, and polling for you - so you don't have to manage these complexities yourself.
+
+ However, if you prefer a notebook or programmatic experience, this guide will show you how to interact with your agents directly using the Python SDK.
+
+## Prerequisites
+
+Before starting, ensure you have:
+
+1. **AgentEx server running locally** (see [Getting Started](https://github.com/scaleapi/scale-agentex#getting-started){target="_blank"})
+2. **AgentEx Python SDK installed**: `uv tool install agentex-sdk`
+3. **Your agent running**: `agentex agents run --manifest manifest.yaml`
+
+## The `dev.ipynb` Notebook
+
+When you run `agentex init`, a `dev.ipynb` notebook is automatically created in your project directory. This notebook is pre-configured with:
+
+- **Client initialization** to connect to your local AgentEx server
+- **Your agent name** already set (no need to manually configure)
+- **Working examples** tailored to your agent type (Sync or Agentic)
+- **Code snippets** demonstrating both streaming and non-streaming patterns
+
+Simply open the notebook and run the cells to start testing your agent immediately. The examples below explain what's in the notebook and how to customize it for your needs.
+
+## Sync ACP Agents
+
+### Lifecycle Overview
+
+Sync ACP agents follow a simple request-response pattern:
+
+- **Send messages** → **Receive immediate responses**
+- Messages are grouped by **tasks** (conversation sessions)
+- If you don't create a task explicitly, one will be created automatically
+- Responses are **synchronous** - you get them immediately after sending
+
+### What's in the Notebook
+
+Your `dev.ipynb` notebook for Sync ACP agents contains the following pre-configured cells:
+
+#### Cell 1: Client Setup (Already Configured)
+
+```python
+from agentex import Agentex
+
+# Connect to your local AgentEx server
+client = Agentex(base_url="http://localhost:5003")
+```
+
+#### Cell 2: Agent Name (Already Set)
+
+```python
+AGENT_NAME = "your-agent-name" # This is pre-filled with your actual agent name
+```
+
+#### Cell 3: (Optional) Create a Task
+
+This cell is commented out by default since task creation is optional for Sync agents. Uncomment if you want to organize messages into a specific task:
+
+```python
+# (Optional) Create a new task. If you don't create a new task,
+# each message will be sent to a new task. The server will create the task for you.
+
+# import uuid
+
+# TASK_ID = str(uuid.uuid4())[:8]
+
+# rpc_response = client.agents.rpc_by_name(
+# agent_name=AGENT_NAME,
+# method="task/create",
+# params={
+# "name": f"{TASK_ID}-task",
+# "params": {}
+# }
+# )
+
+# task = rpc_response.result
+# print(task)
+```
+
+#### Cell 4: Send Messages (Non-Streaming)
+
+This cell demonstrates sending a message and receiving an immediate, complete response. Setting `stream=False` returns complete `TaskMessage` objects - the entire message is available at once with no need to accumulate deltas.
+
+```python
+# Test non streaming response
+from agentex.types import TextContent
+
+# The response is expected to be a list of TaskMessage objects, which is a union of:
+# - TextContent: A message with just text content
+# - DataContent: A message with JSON-serializable data content
+# - ToolRequestContent: A message with a tool request (JSON-serializable)
+# - ToolResponseContent: A message with a tool response
+
+# When processing the message/send response, you can handle different content types
+rpc_response = client.agents.send_message(
+ agent_name=AGENT_NAME,
+ params={
+ "content": {"type": "text", "author": "user", "content": "Hello what can you do?"},
+ "stream": False # Returns complete TaskMessage objects
+ }
+)
+
+if not rpc_response or not rpc_response.result:
+ raise ValueError("No result in response")
+
+# Extract and print just the text content from the response
+for task_message in rpc_response.result:
+ content = task_message.content
+ if isinstance(content, TextContent):
+ text = content.content
+ print(text) # Full text already available - no accumulation needed
+```
+
+#### Cell 5: Send Messages (Streaming)
+
+This cell demonstrates streaming responses in real-time as the agent generates them. Setting `stream=True` returns incremental `TaskMessageUpdate` objects (deltas) that you must accumulate to build the complete message. This is useful for displaying responses as they're generated, providing a better user experience.
+
+```python
+# Test streaming response
+from agentex.types.task_message_update import StreamTaskMessageDelta, StreamTaskMessageFull
+from agentex.types.text_delta import TextDelta
+
+# The result of message/send with stream=True is a TaskMessageUpdate, a union of:
+# - StreamTaskMessageStart: Indicator that streaming started (no useful content)
+# - StreamTaskMessageDelta: A delta of streaming message (contains text delta to aggregate)
+# - StreamTaskMessageDone: Indicator that streaming finished (no useful content)
+# - StreamTaskMessageFull: A non-streaming message (full message, not deltas)
+
+# When processing StreamTaskMessageDelta, you can handle TextDelta, DataDelta,
+# ToolRequestDelta, or ToolResponseDelta depending on what your agent returns
+
+for agent_rpc_response_chunk in client.agents.send_message_stream(
+ agent_name=AGENT_NAME,
+ params={
+ "content": {"type": "text", "author": "user", "content": "Hello what can you do?"},
+ "stream": True # Returns TaskMessageUpdate objects (incremental deltas)
+ }
+):
+ # We know that the result of message/send when stream is True is TaskMessageUpdate
+ task_message_update = agent_rpc_response_chunk.result
+ # Print only the text deltas as they arrive or any full messages
+ if isinstance(task_message_update, StreamTaskMessageDelta):
+ delta = task_message_update.delta
+ if isinstance(delta, TextDelta):
+ # Each delta is a small piece - print immediately for real-time display
+ # Note: If you need the full text, accumulate these deltas yourself
+ print(delta.text_delta, end="", flush=True)
+ else:
+ print(f"Found non-text {type(task_message)} object in streaming message.")
+ elif isinstance(task_message_update, StreamTaskMessageFull):
+ content = task_message_update.content
+ if isinstance(content, TextContent):
+ print(content.content)
+ else:
+ print(f"Found non-text {type(task_message)} object in full message.")
+```
+
+## Agentic ACP Agents
+
+### Lifecycle Overview
+
+Agentic ACP agents work asynchronously:
+
+- **Send events** → **Agent processes when ready** → **Subscribe to responses**
+- Events are like **mobile phone notifications** - asynchronous and non-blocking
+- Agents can **accumulate events** or **process immediately** based on their logic
+- You must **subscribe to responses** rather than waiting for immediate replies
+- **Task creation is required** for all agentic interactions
+
+### What's in the Notebook
+
+Your `dev.ipynb` notebook for Agentic ACP agents contains the following pre-configured cells:
+
+#### Cell 1: Client Setup (Already Configured)
+
+```python
+from agentex import Agentex
+
+# Connect to your local AgentEx server
+client = Agentex(base_url="http://localhost:5003")
+```
+
+#### Cell 2: Agent Name (Already Set)
+
+```python
+AGENT_NAME = "your-agent-name" # This is pre-filled with your actual agent name
+```
+
+#### Cell 3: Create a Task (Required)
+
+For agentic agents, task creation is **required** before sending any events:
+
+```python
+# (REQUIRED) Create a new task. For Agentic agents,
+# you must create a task for messages to be associated with.
+import uuid
+
+rpc_response = client.agents.create_task(
+ agent_name=AGENT_NAME,
+ params={
+ "name": f"{str(uuid.uuid4())[:8]}-task",
+ "params": {}
+ }
+)
+
+task = rpc_response.result
+print(task)
+```
+
+#### Cell 4: Send Events
+
+This cell sends an event to your agent. The agent will process it asynchronously:
+
+```python
+# Send an event to the agent
+
+# The response is expected to be a list of TaskMessage objects, which is a union of:
+# - TextContent: A message with just text content
+# - DataContent: A message with JSON-serializable data content
+# - ToolRequestContent: A message with a tool request (JSON-serializable)
+# - ToolResponseContent: A message with a tool response
+
+# When processing the message/send response, you can handle different content types
+rpc_response = client.agents.send_event(
+ agent_name=AGENT_NAME,
+ params={
+ "content": {"type": "text", "author": "user", "content": "Hello what can you do?"},
+ "task_id": task.id,
+ }
+)
+
+event = rpc_response.result
+print(event)
+```
+
+#### Cell 5: Subscribe to Async Responses
+
+Since agentic agents work asynchronously, use the `subscribe_to_async_task_messages` utility to wait for and display responses:
+
+```python
+# Subscribe to the async task messages produced by the agent
+from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages
+
+task_messages = subscribe_to_async_task_messages(
+ client=client,
+ task=task,
+ only_after_timestamp=event.created_at, # Only get messages after your event
+ print_messages=True, # Automatically print messages as they arrive
+ rich_print=True, # Use rich formatting for better readability
+ timeout=5, # Wait up to 5 seconds for responses
+)
+```
+
+!!! tip "Troubleshooting Async Responses"
+ If no messages appear, your agent might still be processing. Increase `timeout` or **rerun the cell** to continue polling (keep the same `only_after_timestamp`). To see all messages in the conversation, remove the `only_after_timestamp` parameter entirely.
+
+## Understanding Response Types
+
+All RPC methods return a response object with a `.result` field that contains the actual data:
+
+**`send_message`:**
+
+```python
+# Non-streaming (stream=False)
+rpc_response = client.agents.send_message(
+ agent_name=AGENT_NAME,
+ params={"content": {...}, "stream": False}
+)
+task_messages = rpc_response.result # ← List[TaskMessage] - complete messages
+
+# Each TaskMessage contains the full content
+for task_message in task_messages:
+ print(task_message.content) # TextContent, DataContent, etc.
+```
+
+```python
+# Streaming (stream=True)
+for chunk in client.agents.send_message_stream(
+ agent_name=AGENT_NAME,
+ params={"content": {...}, "stream": True}
+):
+ task_message_update = chunk.result # ← TaskMessageUpdate - incremental deltas
+
+ # Handle StreamTaskMessageDelta, StreamTaskMessageFull, etc.
+ if isinstance(task_message_update, StreamTaskMessageDelta):
+ print(task_message_update.delta.text_delta) # Accumulate deltas yourself
+```
+
+**Key difference:** Non-streaming returns complete `TaskMessage` objects, streaming returns `TaskMessageUpdate` deltas that you must accumulate.
+
+**`send_event`:**
+```python
+rpc_response = client.agents.send_event(...)
+event = rpc_response.result # ← .result field contains the Event object
+
+# Event has metadata: id, created_at, task_id, etc.
+print(event.id)
+print(event.created_at)
+```
+
+**Important:** The `Event` object is just confirmation that your event was sent - it does **not** contain the agent's response. The agent processes events asynchronously, so you must use `subscribe_to_async_task_messages()` (see Cell 5 above) to see the agent's actual responses.
+
+**`create_task`:**
+```python
+rpc_response = client.agents.create_task(...)
+task = rpc_response.result # ← .result field contains the Task object
+
+# Task has metadata: id, name, status, created_at, etc.
+print(task.id)
+print(task.name)
+```
+
+**`cancel_task`:**
+```python
+rpc_response = client.agents.cancel_task(...)
+result = rpc_response.result # ← .result field contains a dict
+
+# Dict format: {"message": "Task {task_id} cancelled successfully"}
+print(result["message"])
+```
diff --git a/agentex/docs/docs/development_guides/message_handling.md b/agentex/docs/docs/development_guides/message_handling.md
new file mode 100644
index 00000000..0889ffdc
--- /dev/null
+++ b/agentex/docs/docs/development_guides/message_handling.md
@@ -0,0 +1,145 @@
+# TaskMessages vs LLM Messages
+
+!!! danger "Critical for LLM Integrations"
+ **Agentex stores conversation history as `TaskMessages`, not LLM-compatible messages.** You must convert them to LLM-compatible format before sending to language models.
+
+## Why This Split Exists
+
+**Agentex stores conversation history as `TaskMessages`, not LLM-compatible messages.** This might seem duplicative, but the split between TaskMessage and LLMMessage is intentional and important.
+
+**TaskMessages** are messages that are sent between an Agent and a Client. They are fundamentally decoupled from messages sent to the LLM. This is because you may want to send additional metadata to allow the client to render the message on the UI differently.
+
+**LLMMessages** are OpenAI-compatible messages that are sent to the LLM, and are used to track the state of a conversation with a model.
+
+### ❌ What Doesn't Work
+
+```python
+# WRONG: Trying to send TaskMessages directly to LLM
+@acp.on_message_send
+async def handle_message_send(params: SendMessageParams):
+ # Get conversation history
+ task_messages = await adk.messages.list(task_id=params.task.id)
+
+ # This will FAIL - TaskMessages are not LLM-compatible
+ response = await openai_client.chat.completions.create(
+ model="gpt-4",
+ messages=task_messages # ERROR: Wrong format!
+ )
+```
+
+### ✅ How It Actually Works
+
+Always convert TaskMessages to LLM format:
+
+```python
+# CORRECT: Convert TaskMessages to LLM-compatible format
+from agentex.lib.sdk.utils.messages import convert_task_messages_to_llm_messages
+
+@acp.on_message_send
+async def handle_message_send(params: SendMessageParams):
+ # Get conversation history as TaskMessages
+ task_messages = await adk.messages.list(task_id=params.task.id)
+
+ # Convert to LLM-compatible format
+ llm_messages = convert_task_messages_to_llm_messages(task_messages)
+
+ # Now safe to send to LLM
+ response = await openai_client.chat.completions.create(
+ model="gpt-4",
+ messages=llm_messages # ✅ Correct format
+ )
+```
+
+## Conversion Logic
+
+### Simple Scenarios
+
+In simple scenarios your conversion logic will just use the default converters:
+
+```python
+from agentex.lib.sdk.utils.messages import convert_task_messages_to_llm_messages
+
+# Simple conversion using default converters
+task_messages = await adk.messages.list(task_id=task_id)
+llm_messages = convert_task_messages_to_llm_messages(task_messages)
+```
+
+### Complex Scenarios
+
+However, in complex scenarios where you are leveraging the flexibility of the TaskMessage type to send non-LLM-specific metadata, you should write custom conversion logic.
+
+**Some complex scenarios include:**
+
+- **Postprocessing LLM Output**: Taking a markdown document output by an LLM, postprocessing it into a JSON object to clearly denote title, content, and footers. This can be sent as a `DataContent` TaskMessage to the client and converted back to markdown here to send back to the LLM.
+
+- **Multi-LLM Workflows**: If using multiple LLMs (like in an actor-critic framework), you may want to send `DataContent` that denotes which LLM generated which part of the output and write conversion logic to split the TaskMessage history into multiple LLM conversations.
+
+- **Internal LLM Conversations**: If using multiple LLMs, but one LLM's output should not be sent to the user (i.e. a critic model), you can leverage the State as an internal storage mechanism to store the critic model's conversation history. This is a powerful and flexible way to handle complex scenarios.
+
+### Creating Custom TaskMessage Converters
+
+For complex scenarios, implement the `TaskMessageConverter` base class to create custom conversion logic:
+
+```python
+from agentex.lib.sdk.utils.messages import TaskMessageConverter
+from agentex.types.llm_messages import Message, AssistantMessage
+from agentex.types.task_messages import TaskMessage
+import json
+
+class CustomDataContentConverter(TaskMessageConverter):
+ """Custom converter for DataContent with structured metadata."""
+
+ def convert(self, task_message: TaskMessage) -> Message:
+ """Convert DataContent TaskMessage to LLM format with custom logic."""
+ content = task_message.content
+
+ # Convert structured data to readable format for LLM
+ if isinstance(content.data, dict):
+ formatted_content = f"Analysis: {json.dumps(content.data, indent=2)}"
+ else:
+ formatted_content = str(content.data)
+
+ return AssistantMessage(content=formatted_content)
+
+# Usage with custom converter
+task_messages = await adk.messages.list(task_id=task_id)
+llm_messages = convert_task_messages_to_llm_messages(
+ task_messages,
+ data_converter=CustomDataContentConverter()
+)
+```
+
+## Common Mistakes
+
+### Forgetting to Convert
+```python
+# WRONG: Direct usage without conversion
+task_messages = await adk.messages.list(task_id=task_id)
+response = await llm_client.chat_completion(messages=task_messages) # Will fail!
+
+# CORRECT: Convert first
+llm_messages = convert_task_messages_to_llm_messages(task_messages)
+response = await llm_client.chat_completion(messages=llm_messages)
+```
+
+### Manual Conversion
+```python
+# WRONG: Manual conversion (error-prone, especially for tool messages)
+llm_messages = []
+for task_msg in task_messages:
+ llm_messages.append({
+ "role": "user" if task_msg.content.author == "USER" else "assistant",
+ "content": task_msg.content.content # Missing tool call handling!
+ })
+
+# CORRECT: Use the provided utility
+llm_messages = convert_task_messages_to_llm_messages(task_messages)
+```
+
+## Key Takeaway
+
+!!! info "Remember the Rule"
+ - **TaskMessages**: For Agentex operations (storage, retrieval, agent communication)
+ - **LLM Messages**: For language model API calls (OpenAI, Anthropic, etc.)
+ - **Always convert** between formats when crossing the boundary
+ - **Use custom converters** for complex scenarios with metadata
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/race_conditions.md b/agentex/docs/docs/development_guides/race_conditions.md
new file mode 100644
index 00000000..113e1515
--- /dev/null
+++ b/agentex/docs/docs/development_guides/race_conditions.md
@@ -0,0 +1,402 @@
+# Race Conditions in Agentic ACP
+
+!!! danger "Critical for Production Systems"
+ **All agentic ACP types can experience race conditions that corrupt agent state and cause unpredictable behavior.** Temporal ACP handles these better through singleton workflows and message queuing, but understanding race conditions is crucial for all production systems.
+
+## The Core Problem
+
+In **all agentic ACP types**, multiple events can trigger concurrent processing, leading to race conditions where agents compete for the same resources and corrupt each other's state.
+
+**Temporal ACP** handles this better because workflows are singleton instances with built-in message queuing, while **Base ACP** requires manual race condition handling.
+
+### ❌ What Happens with Race Conditions (All Agentic ACP)
+
+```python
+# Any agentic ACP - Multiple events can trigger simultaneously
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # 🚨 RACE CONDITION: Two events arrive at the same time
+ # Both executions start processing simultaneously
+
+ # Both read the same state
+ state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+
+ # Both process their events
+ processing_count = state.state.get("processing_count", 0)
+ new_count = processing_count + 1
+
+ # Both try to update state with the same value!
+ state.state["processing_count"] = new_count
+ await adk.state.update(state_id=state.id, state=state.state)
+
+ # Result: processing_count = 1 instead of 2 (lost update!)
+```
+
+### ✅ How Temporal ACP Handles This Better
+
+Here's how the actual Temporal workflow from our tutorials handles events and prevents race conditions:
+
+```python
+# From tutorials/10_agentic/10_temporal/010_agent_chat/project/workflow.py
+from temporalio import workflow
+from agentex.core.temporal.workflows.workflow import BaseWorkflow
+from agentex.core.temporal.types.workflow import SignalName
+
+@workflow.defn(name="at010-agent-chat")
+class At010AgentChatWorkflow(BaseWorkflow):
+ """
+ Temporal workflow that handles events sequentially through signals
+ """
+ def __init__(self):
+ super().__init__(display_name="Agent Chat")
+ self._complete_task = False
+ self._state = None
+
+ @workflow.signal(name=SignalName.RECEIVE_EVENT)
+ async def on_task_event_send(self, params: SendEventParams) -> None:
+ # ✅ RACE CONDITION PREVENTION: This signal handler processes
+ # events one at a time in the order they were received
+
+ logger.info(f"Processing event: {params}")
+
+ # Increment turn number - no race condition because only
+ # one signal processes at a time
+ self._state.turn_number += 1
+
+ # Add message to history - safe because sequential processing
+ self._state.input_list.append({
+ "role": "user",
+ "content": params.event.content.content
+ })
+
+ # Process with LLM - each event gets complete, uninterrupted processing
+ run_result = await adk.providers.openai.run_agent_streamed_auto_send(
+ task_id=params.task.id,
+ input_list=self._state.input_list,
+ # ... other params
+ )
+
+ # Update state - safe because no concurrent modifications
+ self._state.input_list = run_result.final_input_list
+
+ @workflow.run
+ async def on_task_create(self, params: CreateTaskParams) -> str:
+ # Initialize state once when workflow starts
+ self._state = StateModel(input_list=[], turn_number=0)
+
+ # Wait for completion condition - workflow runs indefinitely
+ await workflow.wait_condition(
+ lambda: self._complete_task,
+ timeout=None
+ )
+ return "Task completed"
+```
+
+## How Temporal's Event Processing Works
+
+Temporal provides race condition protection through sequential event processing:
+
+```mermaid
+sequenceDiagram
+ participant C1 as Client 1
+ participant C2 as Client 2
+ participant C3 as Client 3
+ participant TS as Temporal Server
+ participant WF as Workflow Instance
+
+ Note over C1,C3: Multiple events arrive simultaneously
+ C1->>TS: Send Event A (10:00:01.100)
+ C2->>TS: Send Event B (10:00:01.150)
+ C3->>TS: Send Event C (10:00:01.200)
+
+ Note over TS: Events queued in order received
+ TS->>WF: Signal: Process Event A
+ Note over WF: Process Event A completely
+ WF-->>TS: Event A complete
+
+ TS->>WF: Signal: Process Event B
+ Note over WF: Process Event B completely
+ WF-->>TS: Event B complete
+
+ TS->>WF: Signal: Process Event C
+ Note over WF: Process Event C completely
+ WF-->>TS: Event C complete
+
+ Note over WF: ✅ No race conditions!
Each event processed atomically
+```
+
+## Temporal's Race Condition Prevention Mechanisms
+
+### 1. Singleton Workflow Instances
+
+- **One workflow per task ID** - Each task gets exactly one workflow instance
+- **Deterministic execution** - Same inputs always produce same results
+- **State isolation** - Each workflow has its own isolated state
+
+### 2. Persistent Event History
+
+Temporal automatically maintains a complete history of every event that happens to your workflow. This is why race conditions are much less likely - every state change is recorded and can be replayed exactly.
+
+**What this means for your agent:**
+
+```python
+# Your agent handles events like this:
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def on_task_event_send(self, params: SendEventParams) -> None:
+ # Every time this runs, Temporal records:
+ # - When the event arrived
+ # - What the event contained
+ # - How your workflow state changed
+
+ self._state.turn_number += 1 # ← This change is recorded
+ self._state.input_list.append({
+ "role": "user",
+ "content": params.event.content.content
+ }) # ← This change is recorded
+
+ # If your workflow crashes and restarts, Temporal will:
+ # 1. Replay all events in the exact same order
+ # 2. Rebuild your state to the exact same point
+ # 3. Continue from where it left off
+```
+
+**Example Event History:**
+```
+10:00:01.100 - Event A: User says "Hello"
+ State: turn_number=1, messages=["Hello"]
+
+10:00:01.150 - Event B: User says "How are you?"
+ State: turn_number=2, messages=["Hello", "How are you?"]
+
+10:00:01.200 - Event C: User says "Goodbye"
+ State: turn_number=3, messages=["Hello", "How are you?", "Goodbye"]
+```
+
+**Why This Prevents Race Conditions:**
+
+- **Deterministic Replay**: If your workflow restarts, events replay in the exact same order
+- **State Consistency**: Your workflow state is always rebuilt the same way
+- **No Lost Events**: Every event is permanently recorded, none can be lost or processed twice
+
+### 3. Sequential Signal Processing
+
+```python
+# In Temporal workflows, this is guaranteed safe:
+@workflow.signal
+async def handle_concurrent_events(self, event_data):
+ # Even if 100 events arrive simultaneously:
+
+ # 1. Read current state - no race condition
+ current_count = self.state.get('count', 0)
+
+ # 2. Process event - exclusive access guaranteed
+ processed_data = await expensive_processing(event_data)
+
+ # 3. Update state - atomic within signal handler
+ self.state['count'] = current_count + 1
+ self.state['last_processed'] = processed_data
+
+ # 4. Next signal waits until this one completes
+ # No lost updates, no race conditions!
+```
+
+### 4. Built-in Queuing and Ordering
+
+```python
+# Multiple events hitting the same workflow:
+# Event A arrives at 10:00:01.100
+# Event B arrives at 10:00:01.150
+# Event C arrives at 10:00:01.200
+
+# Temporal automatically:
+# 1. Queues them in order: [A, B, C]
+# 2. Processes A completely before starting B
+# 3. Processes B completely before starting C
+# 4. Maintains event history for replay/recovery
+
+@workflow.signal
+async def on_task_event_send(self, params):
+ # This handler will process:
+ # - Event A at 10:00:01.100 (processes fully)
+ # - Event B at 10:00:01.500 (waits for A to finish)
+ # - Event C at 10:00:01.800 (waits for B to finish)
+```
+
+## Base ACP Race Condition Solutions
+
+### Using Agent Task Tracker Cursors for Safe Processing
+
+For Base Agentic ACP, you can use **Agent Task Tracker cursors** to coordinate processing and reduce race conditions:
+
+```python
+@acp.on_task_event_send
+async def handle_event_send_with_cursor(params: SendEventParams):
+ # ✅ CURSOR-BASED COORDINATION: Use Agent Task Tracker to track progress
+
+ # Get current processing position
+ tracker = await adk.agent_task_tracker.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+
+ # Get unprocessed events since last cursor (batch processing)
+ unprocessed_events = await adk.events.list_events(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ last_processed_event_id=tracker.last_processed_event_id,
+ limit=10 # Process batches of 10
+ )
+
+ if not unprocessed_events:
+ return # No new events to process
+
+ # ✅ RACE CONDITION REDUCTION: Process events in batches
+ # instead of individual concurrent handlers
+
+ # Process all events in the batch atomically
+ results = []
+ for event in unprocessed_events:
+ result = await process_event(event)
+ results.append(result)
+
+ # Update state with all results at once
+ state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+
+ # Atomic state update with all changes
+ state.state["processing_count"] = state.state.get("processing_count", 0) + len(results)
+ state.state["last_batch_results"] = results
+
+ await adk.state.update(state_id=state.id, state=state.state)
+
+ # ✅ CRITICAL: Update cursor ONLY after processing is complete
+ await adk.agent_task_tracker.update(
+ tracker_id=tracker.id,
+ request=UpdateAgentTaskTrackerRequest(
+ last_processed_event_id=unprocessed_events[-1].id,
+ status_reason=f"Processed batch of {len(unprocessed_events)} events"
+ )
+ )
+```
+
+### Benefits of Cursor-Based Processing
+
+1. **Forward-Only Progress**: Cursors can only move forward, preventing duplicate processing
+2. **Batch Coordination**: Process multiple events atomically instead of concurrent individual handlers
+3. **Resumable Processing**: Pick up where you left off after failures
+4. **State Consistency**: Atomic batch updates reduce concurrent state modifications
+
+### Cursor Safety Rules
+
+!!! danger "Critical Cursor Rules"
+ 1. **Forward-Only Movement**: Never move cursor backwards
+ 2. **Update After Success**: Only update cursor after processing is complete
+ 3. **Atomic Batches**: Process batches atomically to reduce race windows
+ 4. **Single Source of Truth**: Use cursor as the authoritative processing position
+
+### Comparison: Individual vs Batch Processing
+
+```python
+# ❌ RACE-PRONE: Individual event processing
+@acp.on_task_event_send
+async def race_prone_handler(params: SendEventParams):
+ # Each event triggers a separate handler instance
+ # Multiple handlers can run concurrently
+ # Race conditions likely when updating shared state
+ pass
+
+# ✅ RACE-RESISTANT: Cursor-based batch processing
+@acp.on_task_event_send
+async def cursor_batch_handler(params: SendEventParams):
+ # Use current event as trigger to check for batch
+ # Process accumulated events in controlled batches
+ # Update cursor after successful batch processing
+ pass
+```
+
+!!! warning "Not a Complete Solution"
+ Agent Task Tracker cursors **reduce** race conditions but don't eliminate them entirely. For complete race condition prevention, use **Temporal Agentic ACP** which provides guaranteed sequential processing.
+
+## Common Race Condition Scenarios
+
+**1. State Corruption** - Last write wins, updates are lost
+**2. Duplicate Processing** - Same event processed multiple times, wasting resources
+**3. Inconsistent Behavior** - Agent loses context, conversation history gets corrupted
+
+These issues appear in production when users send rapid messages or multiple webhooks arrive simultaneously.
+
+## Base ACP: Manual Race Condition Handling Required
+
+With Base ACP, you must implement your own race condition protection:
+
+### Option 1: Application-Level Locking
+
+```python
+# Use Redis or database locks
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ lock_key = f"task_lock:{params.task.id}:{params.agent.id}"
+
+ async with custom_lock(lock_key, timeout=30):
+ # Only one execution can run at a time
+ state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+ # ... process safely ...
+ await adk.state.update(
+ state_id=state.id,
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ state=state.state
+ )
+```
+
+### Option 2: Event Queuing
+
+Implement your own event queue with separate workers to process events sequentially.
+
+
+## Temporal ACP: Built-in Race Condition Mitigation
+
+Temporal prevents race conditions through:
+
+**1. Singleton Workflow** - One workflow instance per task ID, all events go to same instance
+**2. Sequential Signal Processing** - Signals processed in FIFO order, each completes before next begins
+**3. Workflow-Scoped State** - State in workflow memory (`self._state`), no external database contention
+**4. Built-in Queuing** - Temporal queues events automatically, maintains order
+
+```python
+# Temporal workflow - Race-condition free
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def on_task_event_send(self, params: SendEventParams) -> None:
+ # ✅ Sequential processing guaranteed - no concurrent modifications
+ self._state.conversation_history.append(params.event.content.content)
+ self._state.turn_number += 1
+ # All updates are atomic within signal handler
+```
+
+!!! info "Learn More"
+ See [Temporal Python Message Passing](https://docs.temporal.io/develop/python/message-passing) for advanced concurrency patterns.
+
+## Common Misconceptions
+
+- ❌ **"Careful coding prevents race conditions"** - Race conditions are subtle and only appear under production load
+- ❌ **"Race conditions are rare"** - Common with rapid messages, webhooks, and load-balanced deployments
+- ❌ **"asyncio.Lock solves this"** - Only works within a single process, not across distributed pods
+- ❌ **"Only Base ACP has races"** - All agentic types can, Temporal just handles them better
+
+## Key Takeaway
+
+!!! info "Production Recommendation"
+ - **All agentic ACP**: Can experience race conditions under concurrent load
+ - **Base ACP**: Requires manual distributed locking/queuing mechanisms
+ - **Temporal ACP**: Provides built-in singleton workflows + message queuing for better race condition handling
+ - **Best practice**: Use Temporal ACP for production systems to leverage platform-level concurrency protection
+
+ Learn more about Temporal's message handling: **[Temporal Python Message Passing](https://docs.temporal.io/develop/python/message-passing)**
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/state_machines.md b/agentex/docs/docs/development_guides/state_machines.md
new file mode 100644
index 00000000..c83eda4d
--- /dev/null
+++ b/agentex/docs/docs/development_guides/state_machines.md
@@ -0,0 +1,307 @@
+# State Machines in Agentex
+
+State machines are a fundamental pattern in Agentex for managing complex workflows with multiple states and transitions. This document explains the core concepts and structure of the state machine SDK.
+
+## Overview
+
+A state machine in Agentex consists of:
+
+- **States**: Defined as enums or strings that represent different phases of execution
+- **Workflows**: The logic that executes when a state is active
+- **Transitions**: Movement between states based on workflow execution results
+- **Data**: Persistent data that flows through the state machine
+
+## Core Classes
+
+### StateMachine
+
+The `StateMachine` class is the main orchestrator that manages state transitions and workflow execution.
+
+```python
+class StateMachine(ABC, Generic[T]):
+ def __init__(
+ self,
+ initial_state: str,
+ states: list[State],
+ task_id: str | None = None,
+ state_machine_data: T | None = None,
+ trace_transitions: bool = False,
+ ):
+```
+
+**Key Components:**
+
+- `initial_state`: The starting state name
+- `states`: List of all possible states with their associated workflows
+- `task_id`: Required identifier for tracing and debugging (can be set later)
+- `state_machine_data`: Generic data model that persists throughout execution
+- `trace_transitions`: Enables detailed logging of state transitions
+
+**Execution Flow:**
+
+1. The state machine starts in the `initial_state`
+2. It repeatedly calls `step()` until `terminal_condition()` returns `True`
+3. Each `step()` executes the current state's workflow and transitions to the next state
+
+### State
+
+A `State` represents a single phase in the state machine with an associated workflow:
+
+```python
+class State(BaseModel):
+ name: str
+ workflow: StateWorkflow
+```
+
+**Components:**
+- `name`: Unique identifier for the state
+- `workflow`: The `StateWorkflow` instance that contains the execution logic
+
+### StateWorkflow
+
+`StateWorkflow` is an abstract base class that defines the interface for state execution logic:
+
+```python
+class StateWorkflow(ABC):
+ @abstractmethod
+ async def execute(
+ self, state_machine: "StateMachine", state_machine_data: BaseModel | None = None
+ ) -> str:
+ pass
+```
+
+**Key Methods:**
+- `execute()`: Runs the workflow logic and returns the name of the next state to transition to
+
+## State Transitions and Execution
+
+### How State Transitions Work
+
+1. **Current State Retrieval**: The state machine gets the current state
+2. **Workflow Execution**: Calls `execute()` on the current state's workflow
+3. **Next State Determination**: The workflow returns the name of the next state
+4. **Transition**: The state machine moves to the new state
+
+```python
+async def step(self) -> str:
+ current_state_name = self.get_current_state()
+ current_workflow = self.get_current_workflow()
+
+ next_state_name = await current_workflow.execute(
+ state_machine=self, state_machine_data=self.state_machine_data
+ )
+
+ await self.transition(next_state_name)
+ return next_state_name
+```
+
+### Workflow Execution
+
+Each workflow's `execute()` method:
+
+- Receives the state machine instance and current data
+- Performs the state-specific logic
+- Returns the name of the next state to transition to
+
+**Example Workflow:**
+```python
+class MyWorkflow(StateWorkflow):
+ async def execute(
+ self, state_machine: "StateMachine", state_machine_data: BaseModel | None = None
+ ) -> str:
+ # Perform state-specific logic
+ result = await self.process_data(state_machine_data)
+
+ # Determine next state based on result
+ if result.is_success():
+ return "next_state"
+ else:
+ return "error_state"
+```
+
+## State Definition and Mapping
+
+### Defining States
+
+States are typically defined as enums or constants and mapped to workflows:
+
+```python
+from enum import Enum
+
+class MyStates(str, Enum):
+ START = "start"
+ PROCESSING = "processing"
+ COMPLETE = "complete"
+ ERROR = "error"
+
+# Define workflows for each state
+start_workflow = StartWorkflow()
+processing_workflow = ProcessingWorkflow()
+complete_workflow = NoOpWorkflow() # Terminal state
+error_workflow = ErrorWorkflow()
+
+# Create State objects
+states = [
+ State(name=MyStates.START, workflow=start_workflow),
+ State(name=MyStates.PROCESSING, workflow=processing_workflow),
+ State(name=MyStates.COMPLETE, workflow=complete_workflow),
+ State(name=MyStates.ERROR, workflow=error_workflow),
+]
+```
+
+### State Machine Initialization
+
+```python
+class MyStateMachine(StateMachine[MyDataModel]):
+ async def terminal_condition(self) -> bool:
+ return self.get_current_state() == MyStates.COMPLETE
+
+# Initialize the state machine
+state_machine = MyStateMachine(
+ initial_state=MyStates.START,
+ states=states,
+ state_machine_data=MyDataModel(),
+ trace_transitions=True
+)
+```
+
+## Data Flow
+
+### State Machine Data
+
+The `state_machine_data` parameter is a generic Pydantic model that persists throughout the entire state machine execution:
+
+```python
+class MyDataModel(BaseModel):
+ input_data: str
+ processed_results: list[str] = []
+ error_message: str | None = None
+```
+
+### Data Access in Workflows
+
+Workflows can access and modify the state machine data:
+
+```python
+class ProcessingWorkflow(StateWorkflow):
+ async def execute(
+ self, state_machine: "StateMachine", state_machine_data: MyDataModel | None = None
+ ) -> str:
+ if state_machine_data:
+ # Access and modify the data
+ result = await self.process(state_machine_data.input_data)
+ state_machine_data.processed_results.append(result)
+
+ return "complete"
+ return "error"
+```
+
+## Persistence and Serialization
+
+### Saving State
+
+The state machine can be serialized to a dictionary for persistence:
+
+```python
+saved_state = state_machine.dump()
+# Returns: {
+# "task_id": "task_123",
+# "current_state": "processing",
+# "initial_state": "start",
+# "state_machine_data": {...},
+# "trace_transitions": True
+# }
+```
+
+### Loading State
+
+A state machine can be restored from saved data:
+
+```python
+restored_machine = await MyStateMachine.load(saved_state, states)
+```
+
+## Tracing and Debugging
+
+### Transition Tracing
+
+When `trace_transitions=True`, the state machine logs detailed information about each transition:
+
+- Input state and data
+- Output state and data
+- Transition timing
+- Task correlation
+
+### Task ID Management
+
+The `task_id` is used for correlating traces and debugging. In Temporal workflows, the task_id is often not known until the task handler function receives the task, so you can initialize the state machine without it and set it later:
+
+```python
+# Initialize state machine without task_id
+state_machine = MyStateMachine(
+ initial_state=MyStates.START,
+ states=states,
+ state_machine_data=MyDataModel(),
+ trace_transitions=True
+)
+
+# Set task_id when you receive the task in your handler
+async def handle_task(task: Task) -> TaskResult:
+ state_machine.set_task_id(task.task_id)
+ # ... rest of your task handling logic
+```
+
+**Important:** The task_id is required for tracing to work properly. If you enable `trace_transitions=True` but don't set a task_id, the state machine will raise a `ClientError` when trying to trace transitions.
+
+## Terminal Conditions
+
+The state machine runs until `terminal_condition()` returns `True`. This method must be implemented by subclasses:
+
+```python
+class MyStateMachine(StateMachine[MyDataModel]):
+ async def terminal_condition(self) -> bool:
+ current_state = self.get_current_state()
+ return current_state in ["complete", "error"]
+```
+
+## Common Patterns
+
+### Terminal States
+
+Use `NoOpWorkflow` for states that don't perform any action:
+
+```python
+complete_state = State(name="complete", workflow=NoOpWorkflow())
+```
+
+### Error Handling
+
+Create dedicated error states and workflows:
+
+```python
+class ErrorWorkflow(StateWorkflow):
+ async def execute(self, state_machine, state_machine_data):
+ # Log error, cleanup, etc.
+ return "complete" # or stay in error state
+```
+
+### Conditional Transitions
+
+Workflows can implement complex logic to determine the next state:
+
+```python
+class DecisionWorkflow(StateWorkflow):
+ async def execute(self, state_machine, state_machine_data):
+ if state_machine_data.needs_more_processing():
+ return "processing"
+ elif state_machine_data.has_errors():
+ return "error"
+ else:
+ return "complete"
+```
+
+This foundation provides a robust, type-safe, and traceable framework for building complex workflows in Agentex applications.
+
+## Related Documentation
+
+- **[State Machine Tutorial](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/020_state_machine)**: See how state machines are used in practice with the deep research agent tutorial
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/state_management.md b/agentex/docs/docs/development_guides/state_management.md
new file mode 100644
index 00000000..3441fd08
--- /dev/null
+++ b/agentex/docs/docs/development_guides/state_management.md
@@ -0,0 +1,345 @@
+# State Management
+
+Effective state management is crucial for building complex, stateful agents. This guide covers advanced patterns for managing agent and task state using the `adk.state` module.
+
+## Overview
+
+Agentex provides a powerful state management system that allows you to persist data across agent interactions. State is uniquely identified by the combination of `(task_id, agent_id)`, enabling multiple agents to work on the same task with isolated state.
+
+## Basic State Operations
+
+### Creating State
+
+```python
+from agentex import adk
+from agentex.utils.model_utils import BaseModel
+
+class ConversationState(BaseModel):
+ messages: list[dict] = []
+ user_preferences: dict = {}
+ turn_count: int = 0
+
+@acp.on_task_create
+async def handle_task_create(params: CreateTaskParams):
+ # Initialize state when task is created
+ initial_state = ConversationState(
+ messages=[],
+ user_preferences={},
+ turn_count=0
+ )
+
+ await adk.state.create(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ state=initial_state,
+ trace_id=params.task.id
+ )
+```
+
+### Retrieving State
+
+```python
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Get existing state
+ task_state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ trace_id=params.task.id
+ )
+
+ if task_state:
+ # Convert to your state model
+ state = ConversationState.model_validate(task_state.state)
+ else:
+ # Handle missing state (fallback or error)
+ state = ConversationState()
+```
+
+### Updating State
+
+```python
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Get current state
+ task_state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id
+ )
+
+ state = ConversationState.model_validate(task_state.state)
+
+ # Modify state
+ state.turn_count += 1
+ state.messages.append({
+ "role": "user",
+ "content": params.event.content.content,
+ "timestamp": datetime.utcnow().isoformat()
+ })
+
+ # Save updated state
+ await adk.state.update(
+ state_id=task_state.id,
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ state=state,
+ trace_id=params.task.id
+ )
+```
+
+
+## Task/Agent Scoped State
+
+!!! danger "Critical for Multi-Agent Systems"
+ **State is scoped per (task_id, agent_id) pair.** This isolation is fundamental to Agentex's ability to run multiple agents in parallel without conflicts.
+
+### The Core Principle
+
+Each **unique combination** of `(task_id, agent_id)` gets its own isolated state storage:
+
+```python
+# These are all SEPARATE state instances:
+state_1 = (task_id="task_123", agent_id="agent_A") # Agent A working on Task 123
+state_2 = (task_id="task_123", agent_id="agent_B") # Agent B working on Task 123
+state_3 = (task_id="task_456", agent_id="agent_A") # Agent A working on Task 456
+state_4 = (task_id="task_456", agent_id="agent_B") # Agent B working on Task 456
+```
+
+### Why State Isolation Matters
+
+#### ✅ Enables Parallel Multi-Agent Workflows
+
+```python
+# Task "customer_support_123" with multiple specialized agents
+# Each agent maintains separate, isolated state:
+
+# Customer Support Agent state
+support_state = {
+ "customer_tier": "premium",
+ "issue_category": "billing",
+ "escalation_level": 1,
+ "previous_interactions": [...]
+}
+
+# Technical Diagnostics Agent state
+diagnostics_state = {
+ "tests_completed": ["network_ping", "dns_lookup"],
+ "error_codes": ["timeout_error"],
+ "diagnostic_stage": "investigating_firewall"
+}
+
+# Both work on the same task simultaneously without interference
+await adk.state.create(task_id="customer_support_123", agent_id="support-agent", state=support_state)
+await adk.state.create(task_id="customer_support_123", agent_id="diagnostics-agent", state=diagnostics_state)
+```
+
+#### ✅ Simplifies Agent Logic
+
+Each agent only needs to understand **its own state**:
+
+```python
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Agent only cares about its own state - simple!
+ my_state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id # Only MY state
+ )
+
+ # Simple logic - no coordination needed
+ if my_state.state.get("analysis_complete"):
+ await send_final_report()
+ else:
+ await continue_analysis()
+```
+
+#### ✅ Prevents State Conflicts
+
+```python
+# Multiple agents can update their state simultaneously without conflicts
+async def agent_a_work(task_id: str):
+ state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id="agent_a")
+ state.state["progress"] = "analyzing_data"
+ await adk.state.update(state_id=state.id, state=state.state)
+
+async def agent_b_work(task_id: str):
+ state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id="agent_b")
+ state.state["progress"] = "generating_report"
+ await adk.state.update(state_id=state.id, state=state.state)
+
+# These run in parallel - no conflicts because state is isolated
+await asyncio.gather(
+ agent_a_work("task_123"),
+ agent_b_work("task_123")
+)
+```
+
+## State Access Patterns
+
+#### ✅ Correct: Agent-Scoped State Access
+
+```python
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Always scope state to the current agent
+ my_state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id=params.agent.id # Current agent's ID
+ )
+
+ if my_state:
+ # Update my own state
+ my_state.state["last_processed"] = datetime.now().isoformat()
+ await adk.state.update(state_id=my_state.id, state=my_state.state)
+ else:
+ # Create initial state for this agent on this task
+ await adk.state.create(
+ task_id=params.task.id,
+ agent_id=params.agent.id,
+ state={"initialized": True}
+ )
+```
+
+#### ❌ Wrong: Trying to Access Other Agent's State
+
+```python
+# WRONG: Don't try to access other agents' state directly
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # This violates the isolation principle
+ other_agent_state = await adk.state.get_by_task_and_agent(
+ task_id=params.task.id,
+ agent_id="some_other_agent" # ❌ Don't do this
+ )
+```
+
+#### ✅ Correct: Agent Coordination via Messages
+
+If agents need to coordinate, use the shared message ledger:
+
+```python
+# Agent A signals completion via message
+@acp.on_task_event_send
+async def agent_a_handler(params: SendEventParams):
+ # Complete analysis
+ analysis_result = await perform_analysis()
+
+ # Signal to other agents via shared message ledger
+ await adk.messages.create(
+ task_id=params.task.id,
+ content=DataContent(
+ author=MessageAuthor.AGENT,
+ data={
+ "agent": "analyst",
+ "status": "analysis_complete",
+ "results": analysis_result
+ }
+ )
+ )
+
+# Agent B reacts to signal from message ledger
+@acp.on_task_event_send
+async def agent_b_handler(params: SendEventParams):
+ # Check message ledger for coordination signals
+ messages = await adk.messages.list(task_id=params.task.id)
+
+ analysis_messages = [
+ msg for msg in messages
+ if msg.content.type == TaskMessageContentType.DATA
+ and msg.content.data.get("agent") == "analyst"
+ and msg.content.data.get("status") == "analysis_complete"
+ ]
+
+ if analysis_messages:
+ # Agent A is done - start my work
+ await generate_report(analysis_messages[-1].content.data["results"])
+```
+
+## Multi-Agent Collaboration Example
+
+```python
+# Data Analysis Task with 3 Specialized Agents
+
+# Agent 1: Data Validator
+class DataValidatorAgent:
+ async def process(self, task_id: str, agent_id: str):
+ # My state: validation progress
+ state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id)
+ state.state.update({
+ "validation_stage": "checking_data_quality",
+ "rows_validated": 1500,
+ "errors_found": []
+ })
+ await adk.state.update(state_id=state.id, state=state.state)
+
+# Agent 2: Statistical Analyzer
+class StatisticalAnalyzerAgent:
+ async def process(self, task_id: str, agent_id: str):
+ # My state: analysis progress
+ state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id)
+ state.state.update({
+ "analysis_stage": "computing_correlations",
+ "models_tested": ["linear", "polynomial"],
+ "best_model_r2": 0.85
+ })
+ await adk.state.update(state_id=state.id, state=state.state)
+
+# Agent 3: Report Generator
+class ReportGeneratorAgent:
+ async def process(self, task_id: str, agent_id: str):
+ # My state: report progress
+ state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id)
+ state.state.update({
+ "report_stage": "generating_visualizations",
+ "charts_completed": ["correlation_matrix", "distribution_plots"],
+ "pending_sections": ["conclusions", "recommendations"]
+ })
+ await adk.state.update(state_id=state.id, state=state.state)
+
+# All three agents work in parallel, each maintaining isolated state
+# Coordination happens through the shared message ledger, not shared state
+```
+
+## Common Mistakes
+
+### 1. Trying to Share State Between Agents
+
+```python
+# WRONG: Attempting to share state
+shared_state = {
+ "agent_a_progress": 50,
+ "agent_b_progress": 75
+}
+# This violates the isolation principle
+```
+
+### 2. Not Scoping State Properly
+
+```python
+# WRONG: Missing agent_id in state operations
+state = await adk.state.get_by_task(task_id=task_id) # This doesn't exist!
+
+# CORRECT: Always include both task_id and agent_id
+state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id)
+```
+
+### 3. Coordination Through State Instead of Messages
+
+```python
+# WRONG: Using state for coordination
+my_state.state["signal_to_other_agent"] = "analysis_complete" # Other agents can't see this
+
+# CORRECT: Use messages for coordination
+await adk.messages.create(
+ task_id=task_id,
+ content=DataContent(author=MessageAuthor.AGENT, data={"signal": "analysis_complete"})
+)
+```
+
+## Key Takeaway
+
+!!! info "Remember the Design"
+ - **State is private** to each `(task_id, agent_id)` combination
+ - **Agents work independently** with their own isolated state
+ - **Coordination happens through messages**, not shared state
+ - **This design enables** parallel execution and simple agent logic
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/streaming.md b/agentex/docs/docs/development_guides/streaming.md
new file mode 100644
index 00000000..ab6e1ffe
--- /dev/null
+++ b/agentex/docs/docs/development_guides/streaming.md
@@ -0,0 +1,301 @@
+# Streaming & Delta Accumulation
+
+!!! danger "Critical Concept"
+ **Different ACP types handle streaming and delta accumulation in fundamentally different ways.** Understanding these differences is essential for proper streaming implementation.
+
+## Core Streaming Patterns
+
+### Sync ACP: Server-Side Delta Accumulation
+
+In **Sync ACP**, delta accumulation happens **server-side** using `AsyncGenerator`. The agent yields deltas, but the **server accumulates and persists** them. You can stream multiple messages, just make sure each has a unique index. The server will automatically accumulate and flush all deltas after the final yield:
+
+```python
+# Sync ACP - Return AsyncGenerator, server accumulates deltas
+@acp.on_message_send
+async def handle_message_send(params: SendMessageParams) -> AsyncGenerator[TaskMessageUpdate, None]:
+ # Get conversation history
+ task_messages = await adk.messages.list(task_id=params.task.id)
+ llm_messages = convert_task_messages_to_llm_messages(task_messages)
+
+ message_index = 0
+ async for chunk in adk.providers.litellm.chat_completion_stream(
+ llm_config=LLMConfig(model="gpt-4o-mini", messages=llm_messages, stream=True),
+ trace_id=params.task.id,
+ ):
+ if chunk.choices and chunk.choices[0].delta.content:
+ # YOU yield each delta - SERVER accumulates them
+ yield StreamTaskMessageDelta(
+ index=message_index,
+ delta=TextDelta(text_delta=chunk.choices[0].delta.content)
+ )
+```
+
+### Agentic ACP: Three Streaming Options
+
+Becuase Agentic Agents are completely async, they do not yield nor return anything in their ACP handlers. Instead, they should call the appropriate ADK functions to stream updates to the client.
+
+**Pub/Sub Architecture:**
+Agentex uses a **pub/sub mechanism** in the server to handle Agentic ACP streaming. Clients subscribe to **server-side events (SSE)** from Agentex, which allows them to receive streaming publications from agents in real-time. When an agent streams content, it publishes updates through the ADK, and all subscribed clients receive these updates immediately.
+
+The ADK provides two different streaming approaches for agents behind Agentic ACPs depending on your needs:
+
+## 1. Auto Send (Recommended for Most Cases)
+
+**Agentex handles everything automatically:**
+
+```python
+# Agentic ACP - Auto send handles all delta accumulation
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # Echo user message
+ await adk.messages.create(task_id=params.task.id, content=params.event.content)
+
+ # Auto send: Agentex handles all streaming complexity
+ task_message = await adk.providers.litellm.chat_completion_stream_auto_send(
+ task_id=params.task.id,
+ llm_config=LLMConfig(model="gpt-4o-mini", messages=your_messages, stream=True),
+ trace_id=params.task.id,
+ )
+
+ # Returns complete TaskMessage when streaming finishes
+ # Agentex automatically:
+ # - Creates initial message
+ # - Streams deltas to client
+ # - Accumulates chunks into final content
+ # - Sends StreamTaskMessageDone
+ # - Returns final TaskMessage for your use
+
+ return task_message # Use for state management
+```
+
+**Why Auto Send Exists:**
+
+- **Temporal compatibility**: Temporal workflows cannot yield generators directly (auto send is the easiest option for Temporal ACP)
+- **Automatic processing**: Agentex processes streamed chunks automatically
+- **Content type packaging**: Automatically packages as proper TextContent, DataContent, or ToolRequestContent
+- **Simple**: You don't deal with delta accumulation complexity
+
+## 2. Non-Auto Send (Advanced Control)
+
+**You get full control but handle everything manually:**
+
+```python
+# Agentic ACP - Non-auto send: YOU handle everything
+@acp.on_task_event_send
+async def handle_event_send(params: SendEventParams):
+ # YOU must create the streaming context manually
+ async with adk.streaming.streaming_task_message_context(
+ task_id=params.task.id,
+ initial_content=TextContent(author=MessageAuthor.AGENT, content=""),
+ ) as streaming_context:
+
+ full_response = ""
+ async for chunk in adk.providers.litellm.chat_completion_stream(
+ llm_config=LLMConfig(model="gpt-4o-mini", messages=your_messages, stream=True),
+ trace_id=params.task.id,
+ ):
+ if chunk.choices[0].delta.content:
+ content = chunk.choices[0].delta.content
+ full_response += content # YOU accumulate chunks
+
+ # YOU can transform/filter/analyze each chunk
+ processed_content = content.upper() # Example transformation
+
+ # YOU must manually send each chunk to client
+ await streaming_context.stream_update(
+ update=StreamTaskMessageDelta(
+ parent_task_message=streaming_context.task_message,
+ delta=TextDelta(text_delta=processed_content),
+ ),
+ )
+
+ # Context manager automatically sends StreamTaskMessageDone
+
+ # YOU manually add to conversation state
+ your_conversation_state.append({"role": "assistant", "content": full_response})
+```
+
+**When to Use Non-Auto Send:**
+
+- **Custom processing**: Transform chunks before sending
+- **Analytics**: Track streaming metrics
+- **Multiple destinations**: Send chunks to multiple places
+- **Not supported directly in Temporal workflows**: You CAN use it if you wrap the streaming logic into a custom Temporal activity to avoid crossing network boundaries
+
+!!! info "Custom Activities for Advanced Streaming"
+ If you need custom streaming functionality beyond the basic non-auto send pattern, you'll need to wrap your functionality into **custom activities**. Documentation on creating custom activities is coming soon. For now, reach out to the Agentex maintainers for help with implementing custom streaming solutions.
+
+## Delta Accumulation Flow Comparison
+
+### Sync ACP Flow
+
+```mermaid
+graph LR
+ A[Agent] --> B[Yields Delta]
+ B --> C[Server Receives]
+ C --> D[Server Accumulates]
+ D --> E[Server Persists]
+ E --> F[Client Displays]
+```
+
+1. **Agent yields** individual deltas via `AsyncGenerator`
+2. **Server receives** each `StreamTaskMessageDelta` in `agents_acp_use_case`
+3. **Server accumulates** deltas using `DeltaAccumulator` per message index
+4. **Server persists** complete content on `StreamTaskMessageDone`
+5. **Client displays** streaming updates in real-time
+
+### Agentic Auto Send Flow
+
+```mermaid
+graph LR
+ A[Agent] --> B[Auto Send Activity]
+ B --> C[Agentex Streams]
+ C --> D[Agentex Accumulates]
+ D --> E[Returns TaskMessage]
+```
+
+1. **Agent calls** `chat_completion_stream_auto_send`
+2. **Agentex automatically** handles streaming to client
+3. **Agentex accumulates** chunks internally
+4. **Agent receives** complete `TaskMessage` when done
+
+### Agentic Non-Auto Send Flow
+
+```mermaid
+graph LR
+ A[Agent] --> B[Manual Context]
+ B --> C[Agent Streams]
+ C --> D[Agent Accumulates]
+ D --> E[Context Closes]
+```
+
+1. **Agent creates** streaming context manually
+2. **Agent handles** each chunk individually
+3. **Agent accumulates** and sends deltas
+4. **Context manager** automatically finalizes
+
+## Common Patterns & Mistakes
+
+### ❌ Wrong: Using Non-Auto Send Directly in Temporal Workflow
+
+```python
+# This will FAIL in Temporal workflows - generators can't cross network boundaries
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def on_task_event_send(self, params: SendEventParams):
+ async for chunk in adk.providers.litellm.chat_completion_stream(...):
+ # ERROR: Temporal workflows cannot yield generators directly!
+ pass
+```
+
+### ✅ Correct: Using Auto Send in Temporal
+
+```python
+# This works correctly in Temporal
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def on_task_event_send(self, params: SendEventParams):
+ task_message = await adk.providers.litellm.chat_completion_stream_auto_send(
+ task_id=params.task.id,
+ llm_config=LLMConfig(model="gpt-4o-mini", messages=self._state.input_list, stream=True),
+ trace_id=params.task.id,
+ )
+ # Use task_message.content.content for conversation state
+```
+
+### ✅ Correct: Sync ACP Auto-Flush
+
+```python
+# Sync ACP - Server automatically flushes deltas at end
+@acp.on_message_send
+async def handle_message_send(params: SendMessageParams) -> AsyncGenerator[TaskMessageUpdate, None]:
+ message_index = 0
+ async for chunk in adk.providers.litellm.chat_completion_stream(...):
+ if chunk.choices[0].delta.content:
+ yield StreamTaskMessageDelta(
+ index=message_index,
+ delta=TextDelta(text_delta=chunk.choices[0].delta.content)
+ )
+
+ # Optional: yield StreamTaskMessageDone(index=message_index)
+ # Server will auto-flush accumulated deltas even without DONE signal
+```
+
+## StreamTaskMessageFull Override Technique
+
+You can **override accumulated deltas** by sending `StreamTaskMessageFull` with the same index:
+
+```python
+@acp.on_message_send
+async def streaming_with_override(params: SendMessageParams) -> AsyncGenerator[TaskMessageUpdate, None]:
+ message_index = 0
+
+ # Phase 1: Stream initial response
+ accumulated_content = ""
+ async for chunk in adk.providers.litellm.chat_completion_stream(...):
+ if chunk.choices[0].delta.content:
+ accumulated_content += chunk.choices[0].delta.content
+ yield StreamTaskMessageDelta(
+ index=message_index,
+ delta=TextDelta(text_delta=chunk.choices[0].delta.content)
+ )
+
+ # Phase 2: Override with enhanced content (e.g., add citations)
+ enhanced_content = f"{accumulated_content}\n\n## Sources\n{generate_citations()}"
+
+ # This REPLACES all accumulated deltas for index 0
+ yield StreamTaskMessageFull(
+ index=message_index,
+ content=TextContent(
+ author=MessageAuthor.AGENT,
+ content=enhanced_content
+ )
+ )
+
+ # After StreamTaskMessageFull, no more deltas can be sent to this index
+```
+
+**How Override Works in Server:**
+
+1. **Deltas accumulate**: Server uses `DeltaAccumulator` to collect deltas for each index
+2. **StreamTaskMessageFull arrives**: Server immediately creates final message from `content`
+3. **Index marked complete**: Server adds index to `completed_task_message_indexes`
+4. **Accumulated deltas ignored**: Any `StreamTaskMessageDone` or remaining deltas for that index are discarded
+
+**Use Cases for Override:**
+
+- **Post-processing**: Stream raw content, then enhance with citations/formatting
+- **Analysis**: Stream thinking process, then replace with final conclusion
+- **Tool integration**: Stream partial results, then override with tool-enhanced content
+
+## When to Use Each Pattern
+
+### Use Sync ACP Streaming When:
+
+- Simple request-response patterns
+- Full control over each delta needed
+- Prototyping or development
+- Custom client-side accumulation logic
+
+### Use Agentic Auto Send When:
+
+- **Production systems** (especially Temporal)
+- **Standard streaming** without custom processing
+- **Temporal workflows** (only option)
+- **Simplified development** - let Agentex handle complexity
+
+### Use Agentic Non-Auto Send When:
+
+- **Custom chunk processing** before sending to client
+- **Analytics or monitoring** of streaming chunks
+- **Multiple streaming destinations**
+- **NOT directly in Temporal workflows** (wrap in custom activity or use auto send instead)
+
+## Key Takeaway
+
+!!! info "Choose the Right Pattern"
+
+ - **Sync ACP**: Manual delta accumulation, full control
+ - **Agentic Auto Send**: Automatic everything, works everywhere (easiest option for Temporal)
+ - **Agentic Non-Auto Send**: Manual control for advanced use cases (requires custom Temporal activity wrapper)
+
+
+
\ No newline at end of file
diff --git a/agentex/docs/docs/development_guides/tutorials.md b/agentex/docs/docs/development_guides/tutorials.md
new file mode 100644
index 00000000..ab2be917
--- /dev/null
+++ b/agentex/docs/docs/development_guides/tutorials.md
@@ -0,0 +1,53 @@
+# Tutorials
+
+All hands-on tutorials are maintained in the Agentex Python repository.
+
+[Browse Tutorials on GitHub](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials){target="_blank"}
+
+## Available Tutorials
+
+### Sync ACP (Simple Agents)
+
+Basic agent patterns with simple request-response interactions.
+
+- **[Hello ACP](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/00_sync/000_hello_acp){target="_blank"}**: Your first agent
+- **[Multiturn](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/00_sync/010_multiturn){target="_blank"}**: Conversation history and memory
+- **[Streaming](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/00_sync/020_streaming){target="_blank"}**: Real-time response streaming
+
+### Agentic ACP - Base (Learning & Development)
+
+Stateful workflows with full lifecycle control for development and advanced patterns.
+
+- **[Hello Agentic ACP](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/00_base/000_hello_acp){target="_blank"}**: Three-handler pattern fundamentals
+- **[Multiturn](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/00_base/010_multiturn){target="_blank"}**: Stateful conversations
+- **[Streaming](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/00_base/020_streaming){target="_blank"}**: Response streaming in complex workflows
+- **[Tracing](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/00_base/030_tracing){target="_blank"}**: Observability and debugging
+- **[Other SDKs](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/00_base/040_other_sdks){target="_blank"}**: OpenAI Agents SDK and MCP integration
+- **[Batch Events](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/00_base/080_batch_events){target="_blank"}**: Efficient multi-event handling
+- **[Multi-Agent Assembly Line](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/00_base/090_multi_agent_non_temporal){target="_blank"}**: Multi-agent coordination without Temporal
+
+### Agentic ACP - Temporal (Production)
+
+Enterprise-ready patterns with durable execution for production deployments requiring reliability and fault tolerance.
+
+- **[Hello Temporal](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/000_hello_acp){target="_blank"}**: First Temporal workflow
+- **[Agent Chat](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/010_agent_chat){target="_blank"}**: LLM chat with tools integration
+- **[State Machine](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/020_state_machine){target="_blank"}**: Complex workflow orchestration
+- **[Custom Activities](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/030_custom_activities){target="_blank"}**: Custom activities for external operations
+- **[Agent Chat with Guardrails](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/050_agent_chat_guardrails){target="_blank"}**: Safety and validation for agent conversations
+- **[OpenAI Agents SDK Integration](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/){target="_blank"}**: Production-ready agents with OpenAI SDK + Temporal
+ - [OpenAI Agents SDK: Hello World](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world){target="_blank"}: Automatic durability for LLM calls
+ - [OpenAI Agents SDK: Tools](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools){target="_blank"}: Single and multi-activity tool patterns
+ - [OpenAI Agents SDK: Human-in-the-Loop](https://github.com/scaleapi/agentex-python/tree/main/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop){target="_blank"}: Human approval workflows
+
+## Why Tutorials are on GitHub
+
+✅ **Runnable code**: Clone the repository and run examples immediately
+
+✅ **Always up-to-date**: Code is tested with each SDK release
+
+✅ **Complete projects**: Full file structure and dependencies, not just snippets
+
+✅ **Community contributions**: Submit PRs to improve examples
+
+✅ **Version alignment**: Tutorial code matches the SDK version you're using
diff --git a/agentex/docs/docs/development_guides/webhooks.md b/agentex/docs/docs/development_guides/webhooks.md
new file mode 100644
index 00000000..c8c4f654
--- /dev/null
+++ b/agentex/docs/docs/development_guides/webhooks.md
@@ -0,0 +1,124 @@
+# Webhook Concepts
+
+An agent can respond to events outside of the Agentex ecosystem, such as code being pushed to a GitHub repository. Read below to learn more about how to integrate agents with webhooks.
+
+## Agent Changes
+
+For each event type that you would like the agent to handle, decide on a unique URL path that these events will be forwarded to and whether this will be done via a GET or POST request.
+As an example, suppose we decide to handle code change events as POST requests to the `/code_changes` endpoint.
+The code below will allow the agent to receive the POST requests and handle them appropriately:
+```python
+@acp.post("/code_changes")
+async def handle_code_changes(request: Request) -> JSONResponse:
+ # Handle the code changes here
+ ...
+ # Return a response if desired
+ return JSONResponse(content={"message": "Success"})
+```
+As you can see above, the handler function has access to the full request sent to the agent, including the relevant headers and the payload body if one exists.
+Once all the desired event handlers are added to the agent implementation, and the agent is (re)deployed, the defined routes will be accessible as described below.
+
+## Accessing Routes
+
+To check that the route above has been correctly registered, you should be able to send a POST (or GET, as appropriate) request to `/agents/forward/name/AGENT_NAME/code_changes`.
+If a 404 Not Found response is returned, please confirm that the correct agent name is provided and that the URL path after the agent name matches exactly the route defined for the handler.
+Otherwise, you will receive an Unauthorized response as this route is available but protected to prevent spurious or malicious requests from tainting the agent state and lifecycle.
+Please read below to learn more about how Agent API Keys are used to help authenticate these forward requests.
+
+## API Keys
+
+The forward routes described above rely on Agent API Keys to authenticate the requests and prevent unauthorized access.
+For event providers that allow custom headers to be included with their requests, please create a new key for the agent (see below) and include its value as the `x-agent-api-key` header.
+At request time, the backend will verify both that this key still exists and is associated with the agent that is handling the request before passing it through to the handler implementation.
+
+Not everyone supports including custom headers when sending webhook events - a common alternative is a configured secret which is used to sign these requests which can then be confirmed by the backend.
+The way the signature is calculated differs from product to product - we have implemented this for GitHub and Slack as the two most common use cases, but please let us know if you have another use case which require custom logic.
+
+### Creating API Keys
+
+For now, API key management needs to be done by sending the right requests - cURL examples are given below.
+For configuring API keys in the local environment, it is sufficient to access `localhost:5003` without any authentication.
+To configure the API keys in a deployed environment like `https://chat.sgp.scale.com/`, you will need to make requests to `https://agentex.sgp.scale.com/` with your SGP credentials.
+To get your SGP credentials, you can navigate to `https://app.sgp.scale.com/admin/api-key`, view the SGP API Key shown there and pick the appropriate Account ID from the Accounts tab.
+
+
+### Viewing API Keys
+You can always check to see all of the existing API keys that have been configured for a given agent by running the following command:
+```bash
+# local
+curl http://localhost:5003/agent_api_keys?agent_name=AGENT_NAME
+# remote
+curl https://agentex.sgp.scale.com/agent_api_keys?agent_name=AGENT_NAME \
+ -H "x-api-key: " \
+ -H "x-selected-account-id: "
+```
+
+Note that this will not show you the values of the configured API keys (by design).
+The examples below will just use $HOST instead of the local or remote version - please remember to attach the API key and Account ID headers if you're not doing local development!
+
+#### General
+
+To create an API key that will be included as the `x-agent-api-key` header, pick a memorable name for it (these will be unique per agent) and run the following command:
+```bash
+curl -X POST $HOST/agent_api_keys \
+ -H "Content-Type: application/json" \
+ -d '{
+ "agent_name": "",
+ "name": "mysecretkey"
+ }'
+```
+
+The backend will generate an API key value for you, store this in the database, and return the value for you in the response so that you can set it appropriately.
+You will not be able to retrieve the API key value after you've created the API key so keep it in a safe place!
+
+#### GitHub
+
+To store a GitHub secret that will be used to verify incoming webhook requests, grab the full name of the repository you will be configuring the webhook for and run the following command:
+```bash
+# Replace with your repository name instead of scaleapi/agentex!
+curl -X POST $HOST/agent_api_keys \
+ -H "Content-Type: application/json" \
+ -d '{
+ "agent_name": "",
+ "name": "scaleapi/agentex",
+ "api_key_type": "github"
+ }'
+```
+The backend will generate an API key value for you, store this in the database, and return the value for you in the response so that you can set it in GitHub as the webhook secret.
+If you already have a webhook configured with a secret and you don't want to rotate it, you can also pass it as follows:
+```bash
+# Replace with your repository name instead of scaleapi/agentex
+curl -X POST $HOST/agent_api_keys \
+ -H "Content-Type: application/json" \
+ -d '{
+ "agent_name": "",
+ "name": "scaleapi/agentex",
+ "api_key_type": "github",
+ "api_key": "existingsecret"
+ }'
+```
+
+#### Slack
+To store a Slack secret that will be used to verify incoming webhook requests, you will need:
+- the App ID you will be configuring the webhook for (you can find this under Your Apps at https://api.slack.com/apps/)
+- the Signing Secret, available in the app admin panel under Basic Info
+
+If these are, for example, `A123ABC456` and `abcdefg12345` respectively, then run the following command:
+```bash
+curl -X POST $HOST/agent_api_keys \
+ -H "Content-Type: application/json" \
+ -d '{
+ "agent_name": "",
+ "name": "A123ABC456",
+ "api_key_type": "slack",
+ "api_key": "abcdefg12345"
+ }'
+```
+
+### Deleting API Keys
+If you'd like to remove an existing API key for whatever reason, first grab its ID by listing the keys for the appropriate agent as above.
+Once you have it, just send the following command:
+```bash
+curl -X DELETE $HOST/agent_api_keys/API_KEY_ID
+```
+and the key will be removed, allowing you to make a new one with the same name if desired.
\ No newline at end of file
diff --git a/agentex/docs/docs/getting_started/agentex_overview.md b/agentex/docs/docs/getting_started/agentex_overview.md
new file mode 100644
index 00000000..4a32ab9e
--- /dev/null
+++ b/agentex/docs/docs/getting_started/agentex_overview.md
@@ -0,0 +1,96 @@
+# Agentex Overview
+
+Understanding how Agentex works will help you make better decisions when building and deploying agents.
+
+
+
+## A Helpful Analogy (Formula 1)
+
+Think of Agentex like Formula 1 racing. As an **agent developer**, you're like an F1 team. You want to build the fastest possible car—that's your **agent** (on the right). But no team wants to build the track. You just need a high-performance foundation to race on.
+
+The **Agentex Server** (in the center) is that track. The **dotted lines** in the diagram demonstrate how the track connects everything—all agents are on the same track, so they can race together and be invoked the same way. Agentex handles all the routing, async execution, and infrastructure that keeps agents reliable at scale.
+
+Because of Agentex, agent developers don't have to (and shouldn't) spend time thinking about communication, streaming, credential management, deployment, hosting, or how to move from simple sync agents (L1-L3) to complex async agents (L4+).
+
+**Clients** (on the left)—like chat UIs, API callers, or other systems—are the entities that trigger your agents to perform. They send requests through our unified API. The **ACP** (Agent-to-Client Protocol) is the standardized interface we implemented to ensures every agent communicates the same way, regardless of how it's built internally.
+
+And just like F1 doesn't dictate how teams build their cars, Agentex doesn't dictate how you build agents. Use Scale's SDKs, MCP, OpenAI Agents, LangChain, or any tools you choose. Agentex doesn't replace your tools—it connects them.
+
+The result: you focus on your business logic while Agentex handles everything else.
+
+
+
+## How Agentex Works
+
+In Agentex, agents are "just code". In production, we package up this code as docker images and deploy these agents on Kubernetes using Helm charts (more on this later).
+
+To allow for complete code flexibility, but also make it easy to invoke all agents the same way, we abstract code written by agent developers behind standard entrypoints. We call this abstraction "Agent-to-Client Protocol" (ACP).
+
+Here is an example of how simple this makes it for Agent developers to write arbitrary code behind a standard entrypoint. Here is a Sync Agent's ACP definition:
+
+```python
+acp = FastACP.create(acp_type="sync")
+
+@acp.on_message_send
+async def handle_message_send(params: SendMessageParams) -> Union[TaskMessageContent, list[TaskMessageContent], AsyncGenerator[TaskMessageUpdate, None]]:
+ # Write arbitrary agent logic here
+ pass
+```
+
+The developer just needs to implement this function; calling upon any SDK or resources they'd like and returning the type hinted response shapes. The Agentex server handles data persistence and sending messages back to the client so you don't have to, just focus on your internal logic.
+
+This allows all agent callers (web UIs, APIs, other agents, etc.) to call any Agentex agent the exact same way. Callers only need to know these 3 things:
+
+- **Request payload** (standardized by the ACP)
+- **API route** (domain of the hosted Agentex Server)
+- **The name or ID** of the Agent to call
+
+If you're developing a frontend, we recommend you start with our open source **[Agentex UI](https://github.com/scaleapi/scale-agentex/tree/main/agentex-ui)**. It takes care of the message sending above across all different types of Agents. It also handles stream aggregation logic, and a bunch of other complexity that makes it hard to "get off the ground" with most agent libraries.
+
+### Message Flow
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Agentex Server
+ participant Agent Code
+
+ Client->>Agentex Server: Send Tasks and Messages via unified API
+ Agentex Server->>Agent Code: Route request via ACP
+ Agent Code->>Agent Code: Process using any logic, models, or tools
+ Agent Code-->>Agentex Server: Return response via ACP
+ Agentex Server-->>Client: Stream response to client
+ Note over Client,Agent Code: All agents expose the same API regardless of implementation
+```
+
+## Components
+
+Agentex provides three key components, each with their own open source package:
+
+| Component | Description | Source |
+|-----------|-------------|--------|
+| **Agentic Infrastructure** | Production platform for running agents at scale
• **Agent-to-Client Protocol (ACP)** - Standardized communication
• **Asynchronous Architecture** - Sync and async execution
• **Real-Time Streaming** - Updates to clients
• **Scalable Deployments** - Independent service scaling | [scaleapi/scale-agentex/agentex](https://github.com/scaleapi/scale-agentex/tree/main/agentex){target="_blank"} |
+| **Agentex Python SDK** | Developer toolkit for building agents locally
• **FastACP** - Define handlers with decorators to serve agent code via FastAPI.
• **SDK** - Tools for agent-builders to handle messages, state, tasks, streaming, etc.
• **CLI** - Local development and deployment tools | [scaleapi/scale-agentex-python](https://github.com/scaleapi/scale-agentex-python){target="_blank"} |
+| **Agentex UI** | Frontend components for chat interfaces
• **Agentex UI Kit** - React components for customer apps
• **Agentex Web** - Hosted UI for local development and demos | [scaleapi/scale-agentex/agentex-ui](https://github.com/scaleapi/scale-agentex/tree/main/agentex-ui){target="_blank"} |
+
+
+## Design Principles
+
+### For Developers
+
+We believe the most advanced applications are made code-first. With AI advancements increasingly accessible via SDKs, Agentex prioritizes developer flexibility and rapid iteration without sacrificing capability.
+
+1. **Agents are just code** - Build with complete code flexibility using any programming patterns
+2. **Code is unopinionated and usable with any library** - Use Scale's SDKs, MCP, OpenAI Agents, LangChain, or any tools you choose
+3. **Local development is fast and easy** - Iterate quickly without complex setup or infrastructure
+4. **Both simple sync and complex async use cases are supported** - From basic chatbots (L1-L3) to advanced autonomous agents (L4+)
+5. **All agents can be called with a unified communication protocol** - Standardized ACP ensures consistent invocation across all agent types
+
+### For Customers
+
+Hybrid development creates win-win opportunities for Scale and our partners. Enterprises need the path of least resistance to AI-ify their companies as rapidly as possible. Agentex enables this by opening doors—providing production-grade infrastructure that teams can own and operate independently, rather than creating dependencies on Scale as the sole-developers of enterprise AI.
+
+1. **Deployment process must be acceptable for every enterprise** - Production-grade deployment with Kubernetes and Helm charts
+2. **Agents are modifiable by customers without Scale assistance** - Full code ownership and customization
+3. **Agentic Infrastructure can be used self-service without Scale assistance** - Open-source and independently deployable
+4. **Agentic Infrastructure is well-documented and easy to use for the public** - Comprehensive documentation and examples
diff --git a/agentex/docs/docs/getting_started/choose_your_agent_type.md b/agentex/docs/docs/getting_started/choose_your_agent_type.md
new file mode 100644
index 00000000..b1687604
--- /dev/null
+++ b/agentex/docs/docs/getting_started/choose_your_agent_type.md
@@ -0,0 +1,73 @@
+# Choose Your Agent Type
+
+Agentex supports three agent types with different execution models and capabilities.
+
+## Agent Type Comparison
+
+| Agent Type | [Sync Agents](../acp/sync.md) | [Agentic Agents (Base)](../acp/agentic/base.md) | [Agentic Agents (Temporal)](../acp/agentic/temporal.md) |
+|---------|-------------|----------------|-----------------|
+| **Execution Model** | Blocking, synchronous | Asynchronous, non-blocking | Asynchronous, non-blocking |
+| **Concurrent Requests** | Processes one request at a time | Can process multiple concurrent requests | Can process multiple concurrent requests |
+| **Handler Methods** | Single handler
`on_message_send` | Three handlers
`on_task_create`, `on_task_event_send`, `on_task_cancel` | Three handlers
`on_task_create`, `on_task_event_send`, `on_task_cancel` |
+| **Data Persistence across turns** | via `adk.state` API | via `adk.state` API | Workflow class variables persist automatically
(`adk.state` API usage is optional) |
+| **Message Creation** | Directly return `TaskMessageContent` objects or yield `TaskMessageUpdate` objects
Agentex automatically creates input and output message objects and automatically handles streaming | Explicitly call `adk.messages.create()` | Explicitly call `adk.messages.create()` |
+| **Response Pattern** | Return value becomes agent message | All messages must be created by the agent developer via Agentex Python SDK | All messages must be created by the agent developer via Agentex Python SDK |
+| **Durability** | No durability guarantees. Agent developer must handle crashes and retry idempotency. | No durability guarantees. Agent developer must handle crashes and retry idempotency. | Survives crashes and mid-execution restarts. Agent developer can use Temporal's Python SDK to handle crashes and retries natively. |
+| **Best For** | Simple request-response patterns | Asynchronous workflows, stateful applications | Workflows with complex multi-step tool calls, human-in-the-loop approvals, long-running processes |
+| **Example Use Cases** | FAQ bots, translation services, data lookups | Interactive applications, multi-step analysis, complex business logic | Enterprise integrations, multi-day processes, distributed coordination |
+| **Typical Complexity** | ~30 lines, 1 file | ~80 lines, 1 file | 150+ lines, 4 files |
+
+**Learn more:** [Sync ACP](../acp/sync.md) \| [Agentic ACP](../acp/agentic/overview.md) \| [Temporal Guide](../development_guides/temporal_guide.md) \| [Tutorials](../development_guides/tutorials.md)
+
+---
+
+## Upgrade Path
+
+| Current Type | When to Upgrade | Upgrade To | Key Benefit |
+|--------------|----------------|------------|-------------|
+| **Sync** | Need to handle concurrent requests, perform long running tasks, you find yourself polling or dealing with resource contention issues. | **Agentic (Base)** | Asynchronous execution and state management |
+| **Agentic (Base)** | Need workflows that survive crashes, have complex multi-step tool calls, or need human-in-the-loop approvals | **Agentic (Temporal)** | Durable execution and complex transactional reliability |
+
+## Migration Guides
+
+### Sync → Agentic (Base)
+
+**What changes:**
+
+1. Replace `@acp.on_message_send` handler with three handlers:
+
+ - `@acp.on_task_create` - Initialize task state when task is created
+ - `@acp.on_task_event_send` - Process each incoming event/message
+ - `@acp.on_task_cancel` - Clean up when task is cancelled
+
+2. Change from returning content to explicitly calling `adk.messages.create()` for all messages
+
+3. Update ACP configuration from `acp_type="sync"` to `acp_type="agentic"` with `AgenticACPConfig(type="base")`
+
+!!! tip
+ It's probably easier just to run `agentex init` to bootstrap the skeleton of a new Agentex agent (choose `Agentic - ACP Only` when asked for the type) and then copy over the logic yourself.
+
+**Guide:** [Migration Guide](../concepts/migration_guide.md#sync-to-agentic)
+
+### Agentic (Base) → Agentic (Temporal)
+
+**What changes:**
+
+1. Create new files in your `project/` directory:
+
+ - `workflow.py` - Contains your workflow class with `@workflow.run` and `@workflow.signal` methods
+ - `activities.py` - Contains custom Temporal activities (if needed)
+ - `run_worker.py` - Runs the Temporal worker process
+
+2. Update `acp.py` - Change config from `AgenticACPConfig(type="base")` to `TemporalACPConfig(type="temporal")`
+
+3. Move handler logic from `@acp.on_task_create`, `@acp.on_task_event_send`, `@acp.on_task_cancel` to corresponding workflow methods
+
+4. Store state in workflow class variables (e.g., `self._state`) instead of only using `adk.state` API
+
+!!! tip
+ It's probably easier just to run `agentex init` to bootstrap the skeleton of a new Agentex agent (choose `Agentic - Temporal` when asked for the type) and then copy over the logic yourself.
+
+**Guide:** [Migration Guide](../concepts/migration_guide.md#agentic-to-temporal)
+
+---
diff --git a/agentex/docs/docs/images/agentex_system_diagram.png b/agentex/docs/docs/images/agentex_system_diagram.png
index 949b27ff..fc265dcc 100644
Binary files a/agentex/docs/docs/images/agentex_system_diagram.png and b/agentex/docs/docs/images/agentex_system_diagram.png differ
diff --git a/agentex/docs/docs/images/business_logic.png b/agentex/docs/docs/images/business_logic.png
new file mode 100644
index 00000000..84e8413f
Binary files /dev/null and b/agentex/docs/docs/images/business_logic.png differ
diff --git a/agentex/docs/docs/index.md b/agentex/docs/docs/index.md
index 34fdee36..43a3f289 100644
--- a/agentex/docs/docs/index.md
+++ b/agentex/docs/docs/index.md
@@ -11,54 +11,15 @@ Today, most AI applications are limited to Level 3 (L3) and below, relying on sy
!!! note "Agentex is for all levels of AI agents"
While Agentex is built to support advanced L4+ agents, it natively supports L1–L3 agents and simple request/response agents as well. You can start with basic conversational or task-based AI and seamlessly progress to fully autonomous, distributed, and asynchronous agents—all on the same platform. Agentex is future-proofed for a world where AI will be distributed across all levels.
-## 🚀 Get Started
+## Quick Start
-**Ready to build your first agent?**
+Follow our [Getting Started Guide on GitHub](https://github.com/scaleapi/agentex#getting-started){target="_blank"} to set up your environment and create your first agent.
-**[→ Quick Start Guide on GitHub](https://github.com/scaleapi/scale-agentex#quick-start)** (5 minutes)
+The GitHub README will you how to scaffold an L1 example just to learn the ropes. These Docs are meant to teach you concepts to help you extend all the way to L5 agents. For working code snippets that demonstrate these concepts, refer to the Python SDK. Here is how to use each type of our documentation.
-The README will guide you through:
+| Resource | Description |
+|----------|-------------|
+| **[Agentex README](https://github.com/scaleapi/agentex#getting-started)** | **Getting Started**: Spin up a simple agent on your local computer from scratch in minutes. This comes with a full development UI and agent server. |
+| **[Python SDK](https://github.com/scaleapi/scale-agentex-python)** | **Examples**: Agent-building tutorials that work out of the box. These show how to build up from simple to more complex agents using Agentex. |
+| **[Docs Site](https://agentex.sgp.scale.com/docs)** | **Concepts**: More in depth details on the what, why, and how of building L1-L5 agents.
**Enterprise Support**: Description of how our zero-ops deployment works. Learn how to share hundreds of agents with the rest of your company. Each agent is hosted and scaled independently on cloud-agnostic infrastructure. |
-- Installing prerequisites (Docker, Python 3.12+)
-- Setting up your local development environment
-- Creating and running your first agent with `agentex init`
-- Accessing the web UI to interact with your agent
-
-**Then return here to dive deeper:**
-
-## Learning Path 🎓
-
-### 1. Choose Your Approach
-
-Read [Developing Agentex Agents](developing_agentex_agents.md) to understand the different approaches (Sync, Agentic, Temporal) and decide which fits your use case.
-
-### 2. Learn by Example
-
-**[Browse Tutorials on GitHub](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials)** - Complete, runnable examples:
-
-- **Sync ACP** - Simple request/response agents
-- **Agentic ACP** - Stateful workflows and complex agents
-- **Temporal** - Production-ready enterprise patterns
-
-### 3. Understand the Architecture
-
-Learn how Agentex works at a system level:
-
-- [Architecture Overview](concepts/architecture.md) - System design and message flow
-- [Agent-to-Client Protocol (ACP)](acp/overview.md) - Communication protocol details
-
-!!! warning "Important"
- Pay special attention to the [Critical Concepts](concepts/callouts/overview.md) - they contain essential information about race conditions, message handling, and other implementation details.
-
-### 4. Configure & Deploy
-
-- [Manifest Setup](manifest_setup.md) - Agent configuration and deployment specifications
-- [Deployment Guide](deployment/overview.md) - Deploy using CI/CD pipelines
-
-### 5. API Reference
-
-Keep the [API Reference](api/overview.md) handy for detailed Python SDK documentation when building your agents.
-
----
-
-Scale's Agentic Infrastructure is deployed as part of the [Scale GenAI Platform](https://scale.com/genai-platform). To get your own private enterprise deployment, book a demo with Scale's Enterprise sales team.
diff --git a/agentex/docs/docs/manifest_setup.md b/agentex/docs/docs/manifest_setup.md
index 48a1391b..61f296ce 100644
--- a/agentex/docs/docs/manifest_setup.md
+++ b/agentex/docs/docs/manifest_setup.md
@@ -294,6 +294,7 @@ kubernetes:
```
**Best Practices:**
+
- Include team name for isolation: `{team}-{agent}-{env}`
- Use lowercase with hyphens: `sgp-my-agent-dev`
- Keep under 63 characters (Kubernetes limit)
@@ -307,12 +308,14 @@ auth:
```
**Auth Principal Purpose:**
+
- **User Identification**: Identifies the user/service account deploying the agent
- **Deployment Authorization**: Controls who can deploy agents to specific environments
- **Account/Tenant Isolation**: Associates deployments with specific accounts or tenants
- **Audit Trail**: Tracks who deployed which agents and when
**Best Practices:**
+
- **Unique per environment**: Use different user_id for dev vs prod deployment contexts
- **Environment-specific**: `my-dev-cluster-user-id` vs `my-prod-cluster-user-id`
- **Consistent format**: Use same naming pattern across your organization
@@ -335,6 +338,7 @@ helm_overrides:
```
**Common Overrides:**
+
- **replicaCount**: Number of pod replicas
- **resources**: CPU and memory requests/limits
- **autoscaling**: Horizontal pod autoscaling settings
@@ -396,13 +400,3 @@ EOF
4. **Environment-specific overrides**: Use `--override-file` for different deployment environments
5. **Consistent naming**: Use kebab-case for agent names (e.g., `my-agent`, not `my_agent`)
-## Next Steps
-
-After configuring your manifest:
-
-1. Test locally with `agentex agents run --manifest manifest.yaml`
-2. Build your image with `agentex agents build --manifest manifest.yaml`
-3. Configure secrets with `agentex secrets sync --manifest manifest.yaml --cluster your-cluster`
-4. Deploy with `agentex agents deploy --environment dev --cluster your-cluster --manifest manifest.yaml`
-
-For deployment-specific configuration, see the [Deployment Guide](deployment/overview.md).
diff --git a/agentex/docs/docs/temporal_development/core_concepts.md b/agentex/docs/docs/temporal_development/core_concepts.md
new file mode 100644
index 00000000..f9272a75
--- /dev/null
+++ b/agentex/docs/docs/temporal_development/core_concepts.md
@@ -0,0 +1,113 @@
+# Core Concepts
+
+## Essential Concepts
+
+### 1. Workflows
+
+A [Temporal Workflow](https://docs.temporal.io/concepts/workflows){target="_blank"} contains your agent's business logic with built-in crash resilience.
+
+```python
+# AgentEx Temporal Agent (actual API from codebase)
+@workflow.defn(name="agent-workflow")
+class ConversationWorkflow(BaseWorkflow):
+ def __init__(self):
+ super().__init__()
+ self._complete_task = False
+
+ @workflow.run
+ async def on_task_create(self, params: CreateTaskParams) -> str:
+ # Wait indefinitely for task completion
+ await workflow.wait_condition(
+ lambda: self._complete_task,
+ timeout=None # Can run for days/weeks
+ )
+ return "Task completed"
+
+ @workflow.signal(name=SignalName.RECEIVE_EVENT)
+ async def on_task_event_send(self, params: SendEventParams) -> None:
+ # Process each user message as it arrives
+ await adk.messages.create(...) # AgentEx activity
+```
+
+Workflows use standard Python syntax with Temporal durability guarantees.
+
+### 2. Activities
+
+[Activities](https://docs.temporal.io/concepts/activities){target="_blank"} handle operations that can fail: API calls, database writes, LLM requests.
+
+```python
+# AgentEx handles activities through the ADK
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def on_task_event_send(self, params: SendEventParams) -> None:
+ # AgentEx ADK functions are automatically retried activities
+ result = await adk.providers.openai.run_agent_streamed_auto_send(
+ task_id=params.task.id,
+ input_list=self._state.input_list # Can fail, will retry
+ )
+
+ await adk.messages.create(
+ task_id=params.task.id,
+ content=TextContent(...) # Can fail, will retry
+ )
+```
+
+Activities automatically retry with exponential backoff.
+
+### 3. Workers
+
+[Workers](https://docs.temporal.io/concepts/workers){target="_blank"} are Python processes that execute workflows and activities.
+
+```python
+# AgentEx worker setup (from actual tutorials)
+from agentex.core.temporal.workers.worker import AgentexWorker
+from agentex.core.temporal.activities import get_all_activities
+
+async def main():
+ worker = AgentexWorker(task_queue="my-agent-queue")
+ await worker.run(
+ activities=get_all_activities(), # AgentEx provides activities
+ workflow=MyAgentWorkflow,
+ )
+```
+
+Multiple workers can run for the same agent. Temporal distributes work automatically.
+
+---
+
+## Event Sourcing and Deterministic Replay
+
+### How Temporal Achieves Durable Execution
+
+Temporal uses [event sourcing](https://docs.temporal.io/concepts/event-history){target="_blank"} and deterministic replay to maintain workflow state:
+
+1. **Event logging**: Temporal records every workflow decision as an event
+2. **Crash recovery**: New workers replay all events to recreate exact state
+3. **Seamless resumption**: Workflow continues from the point of interruption
+
+```python
+# What happens during replay:
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def on_task_event_send(self, params: SendEventParams) -> None:
+ # Replay: "We already did this step" (skip)
+ await adk.messages.create(...)
+
+ # Replay: "We already did this step" (skip)
+ result = await adk.providers.openai.run_agent_streamed_auto_send(...)
+
+ # New: "This is where we crashed, continue from here"
+ self._state.turn_number += 1 # ← Resumes here with updated state
+```
+
+Workflow functions run from the beginning during replay, but Temporal skips already-completed steps.
+
+---
+
+## Temporal vs Regular Python: Side-by-Side
+
+| **Regular Python Agent** | **Temporal Agent** |
+|---------------------------|-------------------|
+| Crash = start over | Crash = resume exactly where left off |
+| Manual retry logic | Automatic retries with policies |
+| Cron jobs for scheduling | `workflow.wait_condition()` for long waits |
+| Complex state management | Automatic state persistence |
+| Hard to debug failures | Complete execution history in UI |
diff --git a/agentex/docs/docs/temporal_development/human_in_the_loop.md b/agentex/docs/docs/temporal_development/human_in_the_loop.md
new file mode 100644
index 00000000..c473c8e9
--- /dev/null
+++ b/agentex/docs/docs/temporal_development/human_in_the_loop.md
@@ -0,0 +1,247 @@
+# Human-in-the-Loop Pattern
+
+Enable agents to wait indefinitely for human approval without losing context, using Temporal signals and child workflows.
+
+## The Problem
+
+Your agent needs human approval before taking action, but:
+- Waiting blocks the agent
+- System might crash while waiting
+- Context/state could be lost
+- Can't resume after interruption
+
+**Example scenarios:** Financial transactions, legal document signing, high-stakes decisions requiring compliance approval.
+
+---
+
+## Prerequisites
+
+Before implementing this pattern:
+- OpenAI SDK plugin configured (see [OpenAI SDK Integration Guide](../guides/openai_temporal_integration.md))
+- Understanding of Temporal workflows and signals
+
+---
+
+## Key Concepts
+
+### Signals
+External systems can send messages to running workflows. Think of them as secure, durable event triggers.
+
+**Use cases:** User approvals, webhook notifications, live data feeds, external system updates
+
+**Learn more:** [Temporal Message Passing](https://docs.temporal.io/develop/python/message-passing#send-signal-from-client)
+
+### Child Workflows
+Spawn independent workflows managed by a parent. They inherit all Temporal durability guarantees.
+
+**Use cases:** Long-running sub-processes, parallel operations, approval workflows
+
+**Learn more:** [Temporal Child Workflows](https://docs.temporal.io/develop/python/child-workflows)
+
+---
+
+## The Pattern
+
+### Step 1: Create the Confirmation Tool
+
+```python
+# tools.py
+from agents import function_tool
+from temporalio import workflow
+from temporalio.workflow import ParentClosePolicy
+from project.child_workflow import ApprovalWorkflow
+from agentex.lib.environment_variables import EnvironmentVariables
+
+environment_variables = EnvironmentVariables.refresh()
+
+@function_tool
+async def wait_for_confirmation(action: str) -> str:
+ """Wait for human approval before proceeding"""
+
+ result = await workflow.execute_child_workflow(
+ ApprovalWorkflow.run,
+ environment_variables.WORKFLOW_NAME + "_approval",
+ id=f"approval-{workflow.uuid4()}",
+ parent_close_policy=ParentClosePolicy.TERMINATE,
+ )
+
+ return result
+```
+
+### Step 2: Create Child Workflow for Approval
+
+```python
+# child_workflow.py
+import asyncio
+from temporalio import workflow
+from agentex.lib.utils.logging import make_logger
+from agentex.lib.environment_variables import EnvironmentVariables
+
+environment_variables = EnvironmentVariables.refresh()
+logger = make_logger(__name__)
+
+@workflow.defn(name=environment_variables.WORKFLOW_NAME + "_approval")
+class ApprovalWorkflow:
+ def __init__(self):
+ self._approval_queue: asyncio.Queue[bool] = asyncio.Queue()
+
+ @workflow.run
+ async def run(self, action: str) -> str:
+ logger.info(f"Waiting for approval: {action}")
+
+ # Wait until signal received
+ await workflow.wait_condition(
+ lambda: not self._approval_queue.empty()
+ )
+
+ # Approval received
+ approved = await self._approval_queue.get()
+ return "Approved" if approved else "Rejected"
+
+ @workflow.signal
+ async def approve(self, approved: bool) -> None:
+ """Signal to approve or reject"""
+ await self._approval_queue.put(approved)
+```
+
+### Step 3: Use in Your Agent
+
+```python
+# workflow.py
+from agents import Agent, Runner
+from project.tools import wait_for_confirmation
+
+approval_agent = Agent(
+ name="Approval Agent",
+ instructions="When asked to perform an action, use wait_for_confirmation to get human approval first.",
+ tools=[wait_for_confirmation],
+)
+
+result = await Runner.run(approval_agent, params.event.content.content)
+```
+
+### Step 4: Trigger Approval
+
+Send the signal when human approves:
+
+```bash
+# Via Temporal CLI
+temporal workflow signal \
+ --workflow-id="approval-workflow-id" \
+ --name="approve" \
+ --input=true
+```
+
+Or programmatically:
+
+```python
+# Via Temporal client
+from temporalio.client import Client
+
+client = await Client.connect("localhost:7233")
+handle = client.get_workflow_handle("approval-workflow-id")
+await handle.signal("approve", True)
+```
+
+---
+
+## Why This Works
+
+### Durable Waiting
+
+**Without Temporal:**
+- Agent blocks resources while waiting
+- If system crashes, approval context is lost
+- Have to restart the entire process
+
+**With Temporal:**
+- Agent waits without consuming resources
+- System crashes don't matter - workflow resumes
+- Full context preserved indefinitely
+- Can wait hours, days, or weeks
+
+### Workflow Visualization
+
+**Parent workflow spawns child for approval:**
+
+
+
+The parent workflow waits for the child to complete, which happens when the signal is received.
+
+**Child workflow waits for signal:**
+
+
+
+The child workflow runs until the `approve` signal is triggered, then completes.
+
+---
+
+## When to Use This Pattern
+
+**✅ Use when:**
+- High-stakes decisions requiring human oversight
+- Compliance/audit requirements mandate approval
+- Financial transactions need authorization
+- Legal or regulatory approval workflows
+- Multi-day approval processes
+
+**❌ Don't use when:**
+- Immediate response required
+- Fully automated agents (no human needed)
+- Simple validation (use synchronous checks instead)
+
+---
+
+## Real-World Applications
+
+### Financial Services
+- Large transaction approvals
+- Fraud investigation holds
+- Credit limit increases
+- Account closures
+
+### Healthcare
+- Treatment plan approvals
+- Prescription authorizations
+- Medical record updates
+
+### Enterprise
+- Contract signing workflows
+- Procurement approvals
+- Data access requests
+- Security incident responses
+
+---
+
+## Advanced: Multiple Approval Stages
+
+For workflows requiring multiple approvals:
+
+```python
+# Sequential approvals
+manager_approval = await wait_for_approval("manager")
+if manager_approval == "Approved":
+ director_approval = await wait_for_approval("director")
+```
+
+---
+
+## Game-Changing Benefits
+
+This pattern enables **true transactional agent behavior:**
+
+- **Guaranteed execution** - Workflow will complete even through failures
+- **Exact resumption** - Picks up exactly where it left off
+- **Context preservation** - All state maintained during wait
+- **Indefinite waiting** - Can wait for human input without resource consumption
+
+**Without this foundation, agents remain fragile. With Temporal, they become production-ready systems that seamlessly integrate human oversight.**
+
+---
+
+## See Also
+
+- **[OpenAI SDK Integration Guide](../guides/openai_temporal_integration.md)** - Setup and configuration
+- **[Multi-Activity Tools Pattern](multi_activity_tools.md)** - Combine with multi-step operations
+- **[Temporal Signals](https://docs.temporal.io/develop/python/message-passing)** - Deep dive into signals
+- **[GitHub Example](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/010_temporal/080_open_ai_agents_sdk_human_in_the_loop)** - Full working code
diff --git a/agentex/docs/docs/temporal_development/multi_activity_tools.md b/agentex/docs/docs/temporal_development/multi_activity_tools.md
new file mode 100644
index 00000000..9bb2e8be
--- /dev/null
+++ b/agentex/docs/docs/temporal_development/multi_activity_tools.md
@@ -0,0 +1,190 @@
+# Multi-Activity Tools Pattern
+
+Build atomic multi-step operations within a single tool call using Temporal's sequential activity execution.
+
+## The Problem
+
+You need a tool that performs multiple external operations that must complete together - like transferring money (withdraw + deposit) or processing an order (validate + charge + ship).
+
+**Challenge:** Relying on the LLM to make separate tool calls is unreliable:
+- LLM might only call withdraw, never deposit (money lost!)
+- No guarantee of execution order
+- Can't recover from partial completion
+
+**Solution:** Create one tool with multiple activities executed sequentially.
+
+---
+
+## Prerequisites
+
+Before using this pattern, ensure you have:
+- OpenAI SDK plugin configured (see [OpenAI SDK Integration Guide](../guides/openai_temporal_integration.md))
+- Basic understanding of Temporal activities
+
+---
+
+## The Pattern
+
+### Step 1: Create Individual Activities
+
+```python
+# activities.py
+from temporalio import activity
+import asyncio
+
+@activity.defn
+async def withdraw_money(from_account: str, amount: float) -> str:
+ """Withdraw money from an account"""
+ # Simulates API call (in production: actual banking API)
+ await asyncio.sleep(5)
+ print(f"Withdrew ${amount} from {from_account}")
+ return f"Successfully withdrew ${amount} from {from_account}"
+
+@activity.defn
+async def deposit_money(to_account: str, amount: float) -> str:
+ """Deposit money into an account"""
+ # Simulates API call (in production: actual banking API)
+ await asyncio.sleep(10)
+ print(f"Deposited ${amount} into {to_account}")
+ return f"Successfully deposited ${amount} into {to_account}"
+```
+
+### Step 2: Register Activities
+
+```python
+# run_worker.py
+from agentex.lib.core.temporal.activities import get_all_activities
+from project.activities import withdraw_money, deposit_money
+
+all_activities = get_all_activities() + [withdraw_money, deposit_money]
+
+await worker.run(
+ activities=all_activities,
+ workflow=YourWorkflow,
+)
+```
+
+### Step 3: Create Multi-Activity Tool
+
+```python
+# tools.py
+from agents import function_tool
+from temporalio import workflow
+from datetime import timedelta
+
+@function_tool
+async def move_money(from_account: str, to_account: str, amount: float) -> str:
+ """Move money from one account to another atomically"""
+
+ # Step 1: Withdraw money
+ withdraw_result = await workflow.start_activity(
+ "withdraw_money",
+ args=[from_account, amount],
+ start_to_close_timeout=timedelta(days=1)
+ )
+
+ await withdraw_result
+
+ # Step 2: Deposit money (only happens if withdraw succeeds)
+ deposit_result = await workflow.start_activity(
+ "deposit_money",
+ args=[to_account, amount],
+ start_to_close_timeout=timedelta(days=1)
+ )
+
+ await deposit_result
+
+ return f"Successfully moved ${amount} from {from_account} to {to_account}"
+```
+
+### Step 4: Use the Tool
+
+```python
+# workflow.py
+from agents import Agent, Runner
+from project.tools import move_money
+
+money_agent = Agent(
+ name="Money Transfer Agent",
+ instructions="Use the move_money tool to transfer funds between accounts.",
+ tools=[move_money],
+)
+
+result = await Runner.run(money_agent, params.event.content.content)
+```
+
+---
+
+## Why This Works
+
+### Guaranteed Sequential Execution
+
+```
+1. LLM decides to call move_money tool
+2. Activity: withdraw_money executes
+ → If fails: Temporal retries automatically
+ → If succeeds: Proceeds to next step
+3. Activity: deposit_money executes
+ → If fails: Temporal retries automatically
+ → Withdraw already completed (durable state)
+4. Tool returns success
+5. LLM continues with confirmation
+```
+
+### Transactional Guarantees
+
+- **Atomic operations**: Both complete or workflow handles partial state
+- **Exact resumption**: If system crashes, resumes at exact activity
+- **No lost updates**: Temporal's event sourcing ensures no data loss
+- **Automatic retries**: Failed activities retry without manual intervention
+
+### What You See in Production
+
+
+
+The agent successfully transfers money with full context.
+
+
+
+Temporal UI shows both activities, execution times, and parameters - full observability into the transactional operation.
+
+---
+
+## When to Use This Pattern
+
+**✅ Use when:**
+- Multiple external API calls must complete together
+- Order of operations matters
+- Partial completion is dangerous (e.g., money withdrawn but not deposited)
+- Need guaranteed execution with retries
+- Operations can take minutes/hours to complete
+
+**❌ Don't use when:**
+- Single operation is sufficient
+- Operations are independent (use separate tools instead)
+- Operations are deterministic (no need for activities)
+
+---
+
+## Real-World Examples
+
+### Financial Operations
+- Transfer money (withdraw + deposit)
+- Process payment (authorize + capture)
+- Refund transaction (cancel charge + return funds)
+
+### E-Commerce
+- Process order (validate + charge + create shipment)
+- Cancel order (refund + cancel shipment + update inventory)
+
+### Data Processing
+- ETL pipeline (extract + transform + load)
+- Multi-stage analysis (fetch + process + store)
+
+---
+
+## See Also
+
+- **[OpenAI SDK Integration Guide](../guides/openai_temporal_integration.md)** - Setup and basic usage
+- **[Human-in-the-Loop Pattern](human_in_the_loop.md)** - Add approval to multi-activity operations
+- **[Temporal Activities Docs](https://docs.temporal.io/activities)** - Deep dive into Temporal activities
diff --git a/agentex/docs/docs/temporal_development/openai_integration.md b/agentex/docs/docs/temporal_development/openai_integration.md
new file mode 100644
index 00000000..277b00f7
--- /dev/null
+++ b/agentex/docs/docs/temporal_development/openai_integration.md
@@ -0,0 +1,243 @@
+# OpenAI Agents SDK + Temporal Integration
+
+Learn how to integrate the OpenAI Agents SDK with Temporal workflows in Agentex to build durable, production-grade agents.
+
+!!! note "Temporal Required"
+ The OpenAI Agents SDK integration is **only available with Temporal ACP**. This integration uses Temporal's durability features to make OpenAI SDK calls reliable.
+
+## What You'll Learn
+
+- How to set up the OpenAI Agents SDK plugin with Temporal
+- How to create agents that automatically benefit from Temporal's durability
+- How to add tools that execute as Temporal activities
+- How Temporal provides observability through the Temporal UI
+
+## Why OpenAI SDK + Temporal?
+
+**OpenAI Agents SDK** makes building agents simple - focus on what your agent does, not the infrastructure.
+
+**Temporal** provides durability and fault tolerance - agents survive failures and resume exactly where they left off.
+
+**Together:** You get simple agent development with production-grade reliability. LLM calls, tool executions, and state are all automatically durable.
+
+### The Value
+
+Without Temporal: If your agent crashes mid-conversation, all context is lost.
+
+With Temporal: The agent resumes exactly where it stopped, maintaining full conversation context and state.
+
+**Learn more:** [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) | [Temporal Python](https://docs.temporal.io/develop/python)
+
+---
+
+## Setup & Configuration
+
+### Prerequisites
+
+```bash
+# Create a new Temporal agent
+agentex init
+
+# Choose 'temporal' when prompted
+# Navigate to your project
+cd your-project-name
+```
+
+### Add the OpenAI Plugin
+
+**1. Configure ACP (`acp.py`):**
+
+```python
+from agentex.lib import FastACP
+from agentex.lib.core.temporal.config import TemporalACPConfig
+from agentex.lib.plugins.openai_agents import OpenAIAgentsPlugin
+import os
+
+acp = FastACP.create(
+ acp_type="agentic",
+ config=TemporalACPConfig(
+ type="temporal",
+ temporal_address=os.getenv("TEMPORAL_ADDRESS", "localhost:7233"),
+ plugins=[OpenAIAgentsPlugin()] # Add the plugin
+ )
+)
+```
+
+**2. Configure Worker (`run_worker.py`):**
+
+```python
+from agentex.lib.core.temporal.worker import AgentexWorker
+from agentex.lib.plugins.openai_agents import OpenAIAgentsPlugin
+
+worker = AgentexWorker(
+ task_queue=task_queue_name,
+ plugins=[OpenAIAgentsPlugin()], # Add the plugin
+)
+```
+
+**3. Add OpenAI API Key:**
+
+```yaml
+# In manifest.yaml
+agent:
+ credentials:
+ - env_var_name: "OPENAI_API_KEY"
+ secret_name: "openai-secret"
+ secret_key: "api-key"
+```
+
+That's it! The plugin automatically handles activity creation for all OpenAI SDK calls.
+
+---
+
+## Hello World Example
+
+### Basic Agent Response
+
+```python
+# workflow.py
+from agents import Agent, Runner
+from agentex import adk
+from agentex.lib.types.acp import SendEventParams
+from agentex.lib.core.temporal.types.workflow import SignalName
+from agentex.types.text_content import TextContent
+from temporalio import workflow
+
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def on_task_event_send(self, params: SendEventParams) -> None:
+ # Echo user message
+ await adk.messages.create(task_id=params.task.id, content=params.event.content)
+
+ # Create OpenAI agent
+ agent = Agent(
+ name="Assistant",
+ instructions="You only respond in haikus.",
+ )
+
+ # Run the agent - no need to wrap in activity!
+ result = await Runner.run(agent, params.event.content.content)
+
+ # Send response
+ await adk.messages.create(
+ task_id=params.task.id,
+ content=TextContent(
+ author="agent",
+ content=result.final_output,
+ ),
+ )
+```
+
+### Why No Activity Wrapper?
+
+The OpenAI SDK plugin automatically wraps `Runner.run()` calls in Temporal activities. You get durability without manual activity creation!
+
+### What You'll See
+
+**Agent Response:**
+
+
+
+**Temporal UI - Automatic Activity:**
+
+
+
+The `invoke_model_activity` is created automatically by the plugin, providing full observability.
+
+---
+
+## Tools as Activities
+
+Real agents need tools to interact with external systems. Here's how to add tools that execute as Temporal activities.
+
+### Creating a Tool Activity
+
+```python
+# activities.py
+from temporalio import activity
+
+@activity.defn
+async def get_weather(city: str) -> str:
+ """Get weather for a city (simulates API call)"""
+ if city == "New York City":
+ return "The weather in New York City is 22 degrees Celsius"
+ return "Weather unknown"
+```
+
+### Registering the Activity
+
+```python
+# run_worker.py
+from agentex.lib.core.temporal.activities import get_all_activities
+from project.activities import get_weather
+
+all_activities = get_all_activities() + [get_weather]
+
+await worker.run(
+ activities=all_activities,
+ workflow=YourWorkflow,
+)
+```
+
+### Using the Tool
+
+```python
+# workflow.py
+from agents.workflow import activity_as_tool
+from datetime import timedelta
+from project.activities import get_weather
+
+weather_agent = Agent(
+ name="Weather Assistant",
+ instructions="Use the get_weather tool to answer weather questions.",
+ tools=[
+ activity_as_tool(
+ get_weather,
+ start_to_close_timeout=timedelta(seconds=10)
+ ),
+ ],
+)
+
+result = await Runner.run(weather_agent, params.event.content.content)
+```
+
+### Results
+
+**Agent uses the tool:**
+
+
+
+**Temporal UI shows tool execution:**
+
+
+
+The model invokes the tool, the tool executes as an activity, then the model is called again with the result. All steps are durable.
+
+---
+
+## Advanced Patterns
+
+For production scenarios, check out these design patterns:
+
+### Multi-Activity Tools
+When a single tool needs multiple sequential operations (e.g., withdraw + deposit for money transfer):
+
+**→ See [Multi-Activity Tools Pattern](../design_patterns/multi_activity_tools.md)**
+
+Learn how to create atomic multi-step tools with transactional guarantees.
+
+### Human-in-the-Loop
+When agents need human approval before taking action:
+
+**→ See [Human-in-the-Loop Pattern](../design_patterns/human_in_the_loop.md)**
+
+Learn how to use signals and child workflows for approval workflows that survive system failures.
+
+---
+
+## Complete Examples
+
+**Full working examples on GitHub:**
+- [Hello World](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/010_temporal/060_open_ai_agents_sdk_hello_world)
+- [Tools Integration](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/010_temporal/070_open_ai_agents_sdk_tools)
+- [Human-in-the-Loop](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/010_temporal/080_open_ai_agents_sdk_human_in_the_loop)
+
diff --git a/agentex/docs/docs/temporal_development/overview.md b/agentex/docs/docs/temporal_development/overview.md
new file mode 100644
index 00000000..f81fd395
--- /dev/null
+++ b/agentex/docs/docs/temporal_development/overview.md
@@ -0,0 +1,15 @@
+# Temporal Development
+
+Essential concepts for building durable, enterprise-grade agents with Agentex and Temporal.
+
+> **Note**: All code examples use the actual Agentex + Temporal Python SDK API from the tutorials and codebase.
+
+---
+
+## Why Temporal?
+
+Temporal provides durable execution for AI agents, enabling them to survive crashes, restarts, and scaling events while maintaining state consistency.
+
+[Temporal's durable execution](https://docs.temporal.io/evaluate/understanding-temporal#durable-execution){target="_blank"} ensures workflow continuity regardless of infrastructure failures.
+
+**Learn more**: [Temporal Python SDK Documentation](https://docs.temporal.io/dev-guide/python){target="_blank"}
diff --git a/agentex/docs/docs/temporal_development/troubleshooting.md b/agentex/docs/docs/temporal_development/troubleshooting.md
new file mode 100644
index 00000000..209bf56c
--- /dev/null
+++ b/agentex/docs/docs/temporal_development/troubleshooting.md
@@ -0,0 +1,91 @@
+# Implementation Guide
+
+## Critical Implementation Considerations
+
+### Deterministic Replay Requirements
+
+❌ **Avoid in workflows:**
+```python
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def bad_workflow_handler(self, params):
+ current_time = datetime.now() # Non-deterministic!
+ random_id = uuid.uuid4() # Non-deterministic!
+ api_response = requests.get() # Side effect!
+```
+
+✅ **Correct approach:**
+```python
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def good_workflow_handler(self, params):
+ current_time = workflow.now() # Deterministic
+ random_id = workflow.uuid4() # Deterministic
+ # Side effects go through Agentex ADK (activities)
+ await adk.messages.create(...)
+```
+
+### Activity vs Workflow Boundaries
+
+- **Workflows**: Pure, deterministic business logic
+- **Activities**: All external interactions (APIs, databases, file I/O)
+
+### Common Implementation Issues
+
+#### Data Serialization Limits
+```python
+# ❌ Don't pass huge objects between activities
+@activity.defn
+async def process_data(giant_dataset: List[Dict]): # Can hit 2MB limit
+ ...
+
+# ✅ Pass references instead
+@activity.defn
+async def process_data(dataset_id: str): # Load data inside activity
+ dataset = await load_from_database(dataset_id)
+```
+
+**Why**: Temporal has a 2MB limit per workflow event. Large data structures will fail.
+
+#### Workflow State Size Explosion
+```python
+# ❌ Don't accumulate unbounded state
+async def bad_workflow():
+ all_responses = [] # This grows forever!
+ while True:
+ response = await get_response()
+ all_responses.append(response) # Memory leak in workflow state
+```
+
+**Why**: Workflow state is replayed from the beginning. Growing state = slower replays.
+
+#### Activity Retry Idempotency
+```python
+# ❌ Non-idempotent operations will duplicate
+@activity.defn
+async def send_email(user_id: str):
+ await email_service.send(f"Welcome {user_id}!") # Sends multiple emails on retry
+
+# ✅ Make activities idempotent
+@activity.defn
+async def send_email(user_id: str, idempotency_key: str):
+ await email_service.send_once(key=idempotency_key, ...)
+```
+
+**Why**: Activities retry automatically. Non-idempotent operations will run multiple times.
+
+#### Async/Await in Wrong Places
+```python
+# ❌ Don't await non-Temporal async calls in workflows
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def bad_workflow_handler(self, params):
+ await asyncio.sleep(60) # Will break replay!
+ response = requests.get("...") # Non-deterministic!
+
+# ✅ Use AgentEx ADK for external operations
+@workflow.signal(name=SignalName.RECEIVE_EVENT)
+async def good_workflow_handler(self, params):
+ # All external operations go through ADK (activities)
+ await adk.messages.create(...) # Replay-safe
+ await adk.providers.openai.run_agent_streamed_auto_send(...) # Replay-safe
+```
+
+**Why**: Non-Agentex async operations aren't tracked in event history and break deterministic replay.
diff --git a/agentex/docs/docs/temporal_development/use_cases.md b/agentex/docs/docs/temporal_development/use_cases.md
new file mode 100644
index 00000000..b9c46268
--- /dev/null
+++ b/agentex/docs/docs/temporal_development/use_cases.md
@@ -0,0 +1,416 @@
+# Agent Development with Temporal
+
+Understanding when Temporal becomes essential—and when it's not necessary—helps you choose the right architecture. Here are six scenarios progressing from simple to complex, showing when you don't need Temporal and when it becomes critical:
+
+## 1. Simple Tools
+
+**Scenario:** Basic agent with a simple web search tool. User asks a question, agent uses the tool, returns result.
+
+```mermaid
+flowchart LR
+ subgraph Interaction["User Interaction"]
+ direction TB
+ User([User])
+ Agent[Agent]
+
+ User <--> Agent
+ end
+
+ subgraph Processing["Agent Processing"]
+ direction TB
+ Tool[Tool: Web Search]
+ end
+
+ Agent --> Tool
+
+ style User fill:#e1f5ff
+ style Agent fill:#d4edda
+ style Tool fill:#fff3cd
+```
+
+**Code Example:**
+```python
+from agents import Agent, Runner, WebSearchTool
+
+agent = Agent(
+ name="Assistant",
+ tools=[
+ WebSearchTool(),
+ ],
+)
+
+result = await Runner.run(
+ agent,
+ "Which coffee shop should I go to, taking into account my preferences and the weather today in SF?"
+)
+print(result.final_output)
+```
+
+**Why Temporal is NOT needed:** For simple single-step tools, **Temporal is not necessary**. Base Agentic ACP or even Sync ACP is sufficient for basic operations that complete quickly and don't require durability. Use Base ACP without Temporal for these cases—it's simpler, faster to develop, and has less operational overhead. Temporal only becomes essential when you need the capabilities shown in the scenarios below.
+
+---
+
+## 2. Durability: Complex Multi-Step Tools
+
+**Scenario:** Invoice processing with multiple sequential steps: Download File → Extract Info → Validate Info → Initiate Payment
+
+```mermaid
+graph LR
+ User([User])
+ Agent[Agent]
+ Tool[Tool: Process
Invoice]
+
+ subgraph ToolSteps["Tool Execution Steps"]
+ Step1[Download File]
+ Step2[Extract Info]
+ Step3[Validate Info]
+ Step4[Initiate Payment]
+ end
+
+ User --> Agent
+ Agent --> Tool
+ Tool --> Step1
+ Step1 --> Step2
+ Step2 --> Step3
+ Step3 --> Step4
+
+ style User fill:#e1f5ff
+ style Agent fill:#d4edda
+ style Tool fill:#fff3cd
+ style Step1 fill:#f8d7da
+ style Step2 fill:#f8d7da
+ style Step3 fill:#f8d7da
+ style Step4 fill:#f8d7da
+```
+
+**Code Example:**
+```python
+@function_tool
+def process_invoice(file_uri: str) -> str:
+ file = workflow.execute_activity(download_file, file_uri)
+ vendor_info = workflow.execute_activity(extract, file)
+ valid = workflow.execute_activity(validate, vendor_info)
+ if valid:
+ workflow.execute_activity(pay, vendor_info.vendor)
+ return f"Success: payment sent to {vendor_info.vendor}"
+ else:
+ return "Failure: invalid invoice."
+```
+
+**Why Temporal is needed:** Without Temporal, if the "extract" step fails, you'd have to re-download the file. If "validate" fails, you'd have to re-download AND re-extract. **Temporal persists state after each `execute_activity` call**, so failures only retry the failed step. This is critical for expensive operations (file downloads, API calls, payments) where you can't afford to redo work. Temporal also provides automatic retries with backoff policies for transient failures.
+
+---
+
+## 3. Composability: Human-in-the-Loop
+
+**Scenario:** Invoice processing that requires human approval before proceeding with the full workflow.
+
+```mermaid
+flowchart LR
+ subgraph Interaction["User Interaction"]
+ direction TB
+ User([User])
+ Agent[Agent]
+ Tool[Tool: Process
Invoice]
+
+ User <--> Agent
+ Agent --> Tool
+ end
+
+ subgraph WorkflowSteps["Approval Workflow"]
+ direction TB
+ Approval[Human Approval]
+ Step1[Download File]
+ Step2[Extract Info]
+ Step3[Validate Info]
+ Step4[Initiate Payment]
+
+ Approval --> Step1
+ Step1 --> Step2
+ Step2 --> Step3
+ Step3 --> Step4
+ end
+
+ Tool --> Approval
+
+ style User fill:#e1f5ff
+ style Agent fill:#d4edda
+ style Tool fill:#fff3cd
+ style Approval fill:#ffeaa7
+ style Step1 fill:#f8d7da
+ style Step2 fill:#f8d7da
+ style Step3 fill:#f8d7da
+ style Step4 fill:#f8d7da
+```
+
+**Code Example:**
+```python
+@function_tool
+async def process_invoice(file_uri: str) -> str:
+ return await workflow.execute_child_workflow(...)
+
+@workflow.defn
+class ApproveAndProcessInvoiceWorkflow:
+ def __init__(self, approved: bool = False):
+ self._approved = approved
+
+ @workflow.signal
+ async def on_approval(self, event: ApprovalEvent) -> None:
+ self._approved = event.approved
+
+ @workflow.run
+ async def run(self, file_uri: str) -> str:
+ # re-evaluated when approval signal arrives
+ await workflow.wait_condition(lambda: self._approved)
+ if self._approved:
+ # download, extract, validate, and pay (if valid)
+ else:
+ return "Failure: invalid invoice."
+```
+
+**Why Temporal is needed:** The agent needs to wait for human approval, which could take minutes, hours, or days. **Temporal workflows can pause indefinitely using `wait_condition()` without consuming active resources.** When the human approves (via a signal), the workflow wakes up and continues exactly where it left off. Without Temporal, you'd need to build a complex state machine with polling or webhooks to track where each workflow paused and resume it later. Temporal handles all of this automatically through durable execution and signals.
+
+---
+
+## 4. Autonomous Execution: Scheduled Wake-ups
+
+**Scenario:** Agent schedules a future check (e.g., "check for invoice comments in 3 days"), sleeps, then wakes itself up to continue work.
+
+```mermaid
+flowchart LR
+ subgraph Interaction["User Interaction"]
+ direction TB
+ User([User])
+ Agent[Agent]
+ Tool[Tool: Schedule
for later]
+
+ User <--> Agent
+ Agent --> Tool
+ end
+
+ subgraph DelayedWorkflow["Delayed Child Workflow"]
+ direction TB
+ Wait[Wait delay secs]
+ Ping[Ping OG agent]
+ Wake[Agent wakes up]
+
+ Wait --> Ping
+ Ping --> Wake
+ end
+
+ Tool --> Wait
+ Wake -.-> Agent
+
+ style User fill:#e1f5ff
+ style Agent fill:#d4edda
+ style Tool fill:#fff3cd
+ style Wait fill:#ffeaa7
+ style Ping fill:#f8d7da
+ style Wake fill:#d4edda
+```
+
+**Code Example:**
+```python
+@function_tool
+async def check_for_comments(invoice_id: str, delay: int) -> str:
+ return await workflow.start_child_workflow(
+ DelayedWorkflow, prompt, start_delay=delay
+ )
+
+@workflow.defn
+class DelayedWorkflow:
+ @workflow.run
+ async def run(self, invoice_id: str) -> str:
+ handle = workflow.get_external_workflow_handle_for(AgentWorkflow)
+ await handle.signal(
+ AgentWorkflow.on_event,
+ Event(prompt=f"Resolve comments for invoice: {invoice_id}")
+ )
+
+@workflow.defn
+class AgentWorkflow:
+ @workflow.signal
+ async def on_event(self, event: Event) -> None:
+ # Add event.prompt to Agent inputs and move forwards
+```
+
+**Why Temporal is needed:** Agents need to schedule future actions without polling. **Temporal child workflows can sleep for arbitrary durations (seconds, days, weeks) using `start_delay`, then signal parent workflows to wake them up.** This enables "check back later" patterns without maintaining active connections or cron jobs. Without Temporal, you'd need external scheduling systems, polling mechanisms, or complex timer infrastructure. Temporal makes time-based orchestration a first-class primitive.
+
+---
+
+## 5. Queueing: Async Batch Processing
+
+**Scenario:** Agent receives events from multiple sources (webhooks, users, schedules) and processes them in batches rather than one-by-one.
+
+```mermaid
+flowchart LR
+ subgraph EventSources["Event Sources"]
+ direction TB
+ Sources[Webhooks
Users
Schedules]
+ Event1[1]
+ Event2[2]
+ Event3[3]
+ Event4[4]
+
+ Sources --> Event1
+ Sources --> Event2
+ Sources --> Event3
+ Sources --> Event4
+ end
+
+ subgraph AgentProcessing["Agent Processing"]
+ direction TB
+ Queue[Queue: Events]
+ Process[Process Batch]
+
+ Queue --> Process
+ end
+
+ Event1 --> Queue
+ Event2 --> Queue
+ Event3 --> Queue
+ Event4 --> Queue
+
+ style Sources fill:#e1f5ff
+ style Event1 fill:#fff3cd
+ style Event2 fill:#fff3cd
+ style Event3 fill:#fff3cd
+ style Event4 fill:#fff3cd
+ style Queue fill:#ffeaa7
+ style Process fill:#d4edda
+```
+
+**Code Example:**
+```python
+@workflow.defn
+class AgentWorkflow:
+ @workflow.init
+ def __init__(self, TaskParams):
+ self._queue: asyncio.Queue[Event] = asyncio.Queue()
+ self._batch_size = BATCH_SIZE
+
+ @workflow.signal
+ async def on_event(self, event: Event) -> None:
+ self._queue.put(event)
+
+ @workflow.run
+ async def run(params: TaskParams):
+ # process events as they come in
+ while True:
+ await workflow.wait_condition(
+ lambda: self._queue.qsize() >= self._batch_size,
+ )
+ current_batch = dequeue_all(self._queue, self._batch_size)
+ await workflow.execute_child_workflow( # blocking
+ workflow=ProcessAgentEvents.run,
+ args=[current_batch],
+ )
+```
+
+**Why Temporal is needed:** Processing each event individually is inefficient (API rate limits, batching for analytics, coordinated actions). **Temporal workflows maintain in-memory queues (`asyncio.Queue`) as part of workflow state**, allowing events to accumulate and batch process when thresholds are met. Events received via signals are stored durably in the workflow's event history. Without Temporal, you'd need external queuing systems (Redis, SQS) with complex coordination logic. Temporal makes the queue part of the workflow itself, with automatic persistence and exactly-once processing guarantees.
+
+---
+
+## 6. Long-Running Workflows: Real-World Business Processes
+
+**Scenario:** A 50+ day procurement workflow that wakes and sleeps in response to real-world events:
+
+- **Day 1:** Approve HVAC → Issue PO → Sleep
+- **Day 14:** Check tracking → Detect delay → Surface options → Sleep
+- **Day 15:** Human cancels → Reorder from alternate → Sleep
+- **Day 50:** Windows arrive, steel delayed → Coordinate storage → Sleep
+
+```mermaid
+graph TB
+ Day1["Day 1:
HVAC Approved"]
+ Day14["Day 14:
Check Tracking"]
+ Day15["Day 15:
PM Decision"]
+ Day50["Day 50:
Windows Arrived"]
+
+ Day1 -.Sleep.-> Day14
+ Day14 -.Sleep.-> Day15
+ Day15 -.Sleep.-> Day50
+
+ subgraph Run1["Day 1 Agent Run"]
+ Event1["Wake"]
+ PO1["Issue PO"]
+ Track1["Create Tracking"]
+ Sleep1["Sleep"]
+
+ Event1 --> PO1
+ PO1 --> Track1
+ Track1 --> Sleep1
+ end
+
+ subgraph Run14["Day 14 Agent Run"]
+ Event14["Wake"]
+ Query["Query Vendor
System"]
+ Delay["Detect 3+ Week
Delay"]
+ Options["Surface 3 Options
to PM"]
+ Sleep14["Sleep"]
+
+ Event14 --> Query
+ Query --> Delay
+ Delay --> Options
+ Options --> Sleep14
+ end
+
+ subgraph Run15["Day 15 Agent Run"]
+ Event15["Wake"]
+ Cancel["Cancel Old
Order"]
+ Reorder["Issue New
Order"]
+ Notify["Notify Installation
Crews"]
+ Sleep15["Sleep"]
+
+ Event15 --> Cancel
+ Cancel --> Reorder
+ Reorder --> Notify
+ Notify --> Sleep15
+ end
+
+ subgraph Run50["Day 50 Agent Run"]
+ Event50["Wake"]
+ Detect["Materials Out
of Order"]
+ Coordinate["Coordinate
Storage"]
+ Adjust["Adjust Crew
Schedules"]
+ Sleep50["Sleep..."]
+
+ Event50 --> Detect
+ Detect --> Coordinate
+ Coordinate --> Adjust
+ Adjust --> Sleep50
+ end
+
+ Day1 --> Event1
+ Day14 --> Event14
+ Day15 --> Event15
+ Day50 --> Event50
+
+ style Day1 fill:#e1f5ff
+ style Day14 fill:#e1f5ff
+ style Day15 fill:#e1f5ff
+ style Day50 fill:#e1f5ff
+ style Event1 fill:#d4edda
+ style Event14 fill:#d4edda
+ style Event15 fill:#d4edda
+ style Event50 fill:#d4edda
+ style Sleep1 fill:#e7e7ff
+ style Sleep14 fill:#e7e7ff
+ style Sleep15 fill:#e7e7ff
+ style Sleep50 fill:#e7e7ff
+ style PO1 fill:#fff3cd
+ style Track1 fill:#fff3cd
+ style Query fill:#fff3cd
+ style Delay fill:#f8d7da
+ style Options fill:#fff3cd
+ style Cancel fill:#f8d7da
+ style Reorder fill:#fff3cd
+ style Notify fill:#fff3cd
+ style Detect fill:#f8d7da
+ style Coordinate fill:#fff3cd
+ style Adjust fill:#fff3cd
+```
+
+**Why Temporal is needed:** Business processes span arbitrary timeframes (days, weeks, months) with unpredictable events. **Temporal workflows can run indefinitely, waking in response to external signals (webhooks, scheduled checks, human decisions) while maintaining complete state continuity.** The workflow knows it's on Day 50, which vendor was used, what decisions were made on Day 15, and can access the full history. Without Temporal, you'd need to persist all state externally, build resumption logic, handle partial failures, and orchestrate wake-ups manually. Temporal makes long-running, event-driven processes as simple as writing sequential code with signals and wait conditions.
+
+**Key Insight:** Most business-critical processes are long and dynamic. They must adapt to unpredictable, real-world events. Agentex, with its native Temporal integration, gives AI agents the critical capabilities needed to orchestrate complex workflows.
diff --git a/agentex/docs/docs/tutorials.md b/agentex/docs/docs/tutorials.md
index d13ce469..0f63b362 100644
--- a/agentex/docs/docs/tutorials.md
+++ b/agentex/docs/docs/tutorials.md
@@ -81,9 +81,3 @@ Before starting any tutorial:
2. **Development environment set up**: Follow the [Quick Start Guide](https://github.com/scaleapi/scale-agentex#quick-start)
3. **Basic Python knowledge**: Familiarity with async/await patterns
-## Next Steps
-
-- **[Choosing Your Approach](developing_agentex_agents.md)** - Decide which tutorials to follow
-- **[Agent-to-Client Protocol (ACP)](acp/overview.md)** - Understand the theory behind the examples
-- **[API Reference](api/overview.md)** - Look up SDK functions while building
-- **[Deployment Guide](deployment/overview.md)** - Deploy your agent when ready
diff --git a/agentex/docs/mkdocs.yml b/agentex/docs/mkdocs.yml
index 7490d42e..fe6b8b5d 100644
--- a/agentex/docs/mkdocs.yml
+++ b/agentex/docs/mkdocs.yml
@@ -27,6 +27,7 @@ extra_javascript:
- extra-loader.js
markdown_extensions:
- admonition
+ - attr_list
- codehilite
- toc:
permalink: true
@@ -46,22 +47,16 @@ markdown_extensions:
nav:
- Introduction: index.md
- Getting Started:
- - Choosing Your Approach: developing_agentex_agents.md
- - Architecture Overview: concepts/architecture.md
- - Development Guides:
- - Tutorials: tutorials.md
- - Temporal Guide: temporal-guide.md
- - OpenAI Agents SDK + Temporal: guides/openai_temporal_integration.md
- - Migration: concepts/migration_guide.md
- - Jupyter Notebooks: local_development/notebooks.md
- - Agent Client Protocol:
- - Overview: acp/overview.md
- - Sync ACP: acp/sync.md
+ - Architecture Overview: getting_started/agentex_overview.md
+ - Choose Your Agent Type: getting_started/choose_your_agent_type.md
+ - Agent Types:
+ - Agent to Client Protocol (ACP): agent_types/overview.md
+ - Sync ACP: agent_types/sync.md
- Agentic ACP:
- - Overview: acp/agentic/overview.md
- - Base Implementation: acp/agentic/base.md
- - Temporal Implementation: acp/agentic/temporal.md
- - Best Practices: acp/best-practices.md
+ - Overview: agent_types/agentic/overview.md
+ - Base Implementation: agent_types/agentic/base.md
+ - Temporal Implementation: agent_types/agentic/temporal.md
+ - Migration Guide: agent_types/agent_type_migration_guide.md
- Core Concepts:
- Agents: concepts/agents.md
- Tasks: concepts/task.md
@@ -69,25 +64,30 @@ nav:
- Events: concepts/events.md
- State: concepts/state.md
- Streaming: concepts/streaming.md
- - Critical Concepts:
- - Overview: concepts/callouts/overview.md
- - Delta Accumulation: concepts/callouts/streaming.md
- - Race Conditions: concepts/callouts/race_conditions.md
- - Message Handling: concepts/callouts/message_handling.md
- - Events vs Messages: concepts/callouts/events_vs_messages.md
- - State Scoping: concepts/callouts/state_management.md
- - Advanced Concepts:
- - State Machines: concepts/state_machines.md
- - Agent Task Tracker: concepts/agent_task_tracker.md
- - Webhooks: concepts/webhooks.md
- - Design Patterns:
- - Overview: design_patterns/overview.md
- - Multi-Activity Tools: design_patterns/multi_activity_tools.md
- - Human-in-the-Loop: design_patterns/human_in_the_loop.md
+ - Development Guides:
+ - Tutorials: development_guides/tutorials.md
+ - Jupyter Notebooks (dev.ipynb): development_guides/jupyter_notebooks.md
+ - Streaming: development_guides/streaming.md
+ - Race Conditions: development_guides/race_conditions.md
+ - Message Handling: development_guides/message_handling.md
+ - Events vs Messages: development_guides/events_vs_messages.md
+ - State Scoping: development_guides/state_management.md
+ - State Machines: development_guides/state_machines.md
+ - Agent Task Tracker: development_guides/agent_task_tracker.md
+ - Webhooks: development_guides/webhooks.md
+ - Temporal Development:
+ - Overview: temporal_development/overview.md
+ - Use Cases: temporal_development/use_cases.md
+ - Core Concepts: temporal_development/core_concepts.md
+ - Multi-Activity Tools: temporal_development/multi_activity_tools.md
+ - Human-in-the-Loop: temporal_development/human_in_the_loop.md
+ - OpenAI Integration: temporal_development/openai_integration.md
+ - Troubleshooting: temporal_development/troubleshooting.md
- Deployment:
- Overview: deployment/overview.md
- Commands Reference: deployment/commands.md
- Manifest Configuration: manifest_setup.md
+ - Integrate CI/CD: deployment/cicd.md
- API Reference:
- Overview: api/overview.md
- Agent Development Kit: api/adk.md
diff --git a/agentex/src/api/schemas/tasks.py b/agentex/src/api/schemas/tasks.py
index d94b3eb2..df5ad42a 100644
--- a/agentex/src/api/schemas/tasks.py
+++ b/agentex/src/api/schemas/tasks.py
@@ -64,7 +64,7 @@ class TaskResponse(Task):
agents: list["Agent"] | None = Field(
default=None,
- title="Agents associated with this task (only populated when 'agents' view is requested)",
+ title="Agents associated with this task (only populated when 'agent' view is requested)",
)
diff --git a/agentex/src/domain/repositories/task_repository.py b/agentex/src/domain/repositories/task_repository.py
index a228501e..40c8781e 100644
--- a/agentex/src/domain/repositories/task_repository.py
+++ b/agentex/src/domain/repositories/task_repository.py
@@ -40,7 +40,7 @@ async def list_with_join(
agent_id: str | None = None,
agent_name: str | None = None,
order_by: str | None = None,
- order_direction: Literal["asc", "desc"] = "desc",
+ order_direction: Literal["asc", "desc"] = "asc",
limit: int | None = None,
page_number: int | None = None,
relationships: list[TaskRelationships] | None = None,