diff --git a/agentex/database/migrations/alembic/versions/2025_10_30_2157_change_default_acp_to_async_329fbafa4ff9.py b/agentex/database/migrations/alembic/versions/2025_10_30_2157_change_default_acp_to_async_329fbafa4ff9.py new file mode 100644 index 0000000..d0a16c0 --- /dev/null +++ b/agentex/database/migrations/alembic/versions/2025_10_30_2157_change_default_acp_to_async_329fbafa4ff9.py @@ -0,0 +1,42 @@ +"""change_default_acp_to_async + +Revision ID: 329fbafa4ff9 +Revises: d7addd4229e8 +Create Date: 2025-10-30 21:57:45.309656 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '329fbafa4ff9' +down_revision: Union[str, None] = 'd7addd4229e8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Change the server_default of acp_type column to "async" + op.alter_column( + 'agents', + 'acp_type', + server_default='async', + existing_nullable=False + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Revert the server_default of acp_type column back to "agentic" + op.alter_column( + 'agents', + 'acp_type', + server_default='agentic', + existing_nullable=False + ) + # ### end Alembic commands ### diff --git a/agentex/database/migrations/migration_history.txt b/agentex/database/migrations/migration_history.txt index d0be802..b6a9c8a 100644 --- a/agentex/database/migrations/migration_history.txt +++ b/agentex/database/migrations/migration_history.txt @@ -1,4 +1,5 @@ -09368a02d6cc -> d7addd4229e8 (head), soft delete status +d7addd4229e8 -> 329fbafa4ff9 (head), change_default_acp_to_async +09368a02d6cc -> d7addd4229e8, soft delete status 739800d3e1ce -> 09368a02d6cc, deployment history dbac39ab82c3 -> 739800d3e1ce, registration metadata 87454c35c13e -> dbac39ab82c3, add_task_metadata diff --git a/agentex/docs/docs/acp/agentic/base.md b/agentex/docs/docs/acp/async/base.md similarity index 91% rename from agentex/docs/docs/acp/agentic/base.md rename to agentex/docs/docs/acp/async/base.md index bacd948..20e73c1 100644 --- a/agentex/docs/docs/acp/agentic/base.md +++ b/agentex/docs/docs/acp/async/base.md @@ -1,6 +1,6 @@ -# Base Agentic ACP +# Base Async ACP -**Base Agentic ACP** is the foundational implementation designed for learning and development environments. It provides complete control over task lifecycle while using Agentex's built-in state management. +**Base Async ACP** is the foundational implementation designed for learning and development environments. It provides complete control over task lifecycle while using Agentex's built-in state management. ## When to Use @@ -25,14 +25,14 @@ ### Not Ideal For: -❌ **Production Critical Systems** - Use Temporal Agentic ACP +❌ **Production Critical Systems** - Use Temporal Async ACP ❌ **Very Large State Requirements** - Consider external storage ❌ **Enterprise Durability** - No advanced fault tolerance ❌ **High-Scale Operations** - Better suited for development ## State Management -Base Agentic ACP requires using Agentex's state management system: +Base Async ACP requires using Agentex's state management system: ```python @acp.on_task_create @@ -187,7 +187,7 @@ Used in `@acp.on_task_cancel` for cleanup: ## Next Steps -- **Ready for production?** Learn about [Temporal Agentic ACP](temporal.md) +- **Ready for production?** Learn about [Temporal Async ACP](temporal.md) - **Need to upgrade?** See the [Migration Guide](../../concepts/migration_guide.md) - **New to Agentex?** Follow the [Quick Start Guide on GitHub](https://github.com/scaleapi/scale-agentex#quick-start) - **Ready to build?** Check out [Base Agentic Tutorials on GitHub](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/000_base) diff --git a/agentex/docs/docs/acp/agentic/overview.md b/agentex/docs/docs/acp/async/overview.md similarity index 82% rename from agentex/docs/docs/acp/agentic/overview.md rename to agentex/docs/docs/acp/async/overview.md index fc3e7bc..6cee37d 100644 --- a/agentex/docs/docs/acp/agentic/overview.md +++ b/agentex/docs/docs/acp/async/overview.md @@ -1,10 +1,10 @@ -# Agentic ACP +# Async 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. +**Async 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. -## What is Agentic ACP? +## What is Async ACP? -Agentic ACP provides full lifecycle control where: +Async ACP provides full lifecycle control where: - You implement **three handler methods** for complete lifecycle management - Tasks require **explicit initialization** and cleanup @@ -13,9 +13,9 @@ Agentic ACP provides full lifecycle control where: ## Two Implementation Approaches -Agentic ACP can be implemented in two ways: +Async ACP can be implemented in two ways: -| Aspect | Base Agentic ACP | Temporal Agentic ACP | +| Aspect | Base Async ACP | Temporal Async ACP | |--------|------------------|----------------------| | **Best for** | Learning, development, POCs | Production, enterprise | | **State Management** | Agentex state APIs | Temporal event sourcing or Agentex state | @@ -26,7 +26,7 @@ Agentic ACP can be implemented in two ways: ## Core Architecture -Both Base and Temporal Agentic ACP share the same three-handler pattern: +Both Base and Temporal Async ACP share the same three-handler pattern: ```python @acp.on_task_create @@ -68,7 +68,7 @@ graph TD ## 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. +Think of Async 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 @@ -148,24 +148,24 @@ async def handle_event_send(params: SendEventParams): ## Choose Your Implementation -### Base Agentic ACP +### Base Async ACP Perfect for learning and development. Use Agentex's built-in state management. -**[→ Read Base Agentic ACP Guide](base.md)** +**[→ Read Base Async ACP Guide](base.md)** -### Temporal Agentic ACP +### Temporal Async ACP Production-ready with durable execution and automatic fault tolerance. -**[→ Read Temporal Agentic ACP Guide](temporal.md)** +**[→ Read Temporal Async ACP Guide](temporal.md)** --- ## Next Steps -- **Getting started?** Learn about [Base Agentic ACP](base.md) first -- **Ready for production?** Explore [Temporal Agentic ACP](temporal.md) +- **Getting started?** Learn about [Base Async ACP](base.md) first +- **Ready for production?** Explore [Temporal Async ACP](temporal.md) - **Need to upgrade?** Check the [Migration Guide](../../concepts/migration_guide.md) - **New to Agentex?** Follow the [Quick Start Guide on GitHub](https://github.com/scaleapi/scale-agentex#quick-start) - **Ready to build?** Check out [Agentic Tutorials on GitHub](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic) diff --git a/agentex/docs/docs/acp/agentic/temporal.md b/agentex/docs/docs/acp/async/temporal.md similarity index 95% rename from agentex/docs/docs/acp/agentic/temporal.md rename to agentex/docs/docs/acp/async/temporal.md index 6544249..cdec5a3 100644 --- a/agentex/docs/docs/acp/agentic/temporal.md +++ b/agentex/docs/docs/acp/async/temporal.md @@ -1,6 +1,6 @@ -# Temporal Agentic ACP +# Temporal Async ACP -**Temporal Agentic ACP** provides production-ready agent development with **durable execution**, **fault tolerance**, and **automatic state management**. Instead of implementing ACP handlers directly, you implement Temporal workflows that are automatically integrated with the ACP protocol. +**Temporal Async ACP** provides production-ready agent development with **durable execution**, **fault tolerance**, and **automatic state management**. Instead of implementing ACP handlers directly, you implement Temporal workflows that are automatically integrated with the ACP protocol. ## When to Use @@ -26,11 +26,11 @@ ### Not Ideal For: -❌ **Learning Agentex basics** - Start with Base Agentic ACP +❌ **Learning Agentex basics** - Start with Base Async ACP ❌ **Simple prototypes** - Higher complexity overhead ❌ **Development without Temporal** - Requires Temporal infrastructure -## Key Differences from Base Agentic ACP +## Key Differences from Base Async ACP ### No Manual ACP Handlers @@ -110,7 +110,7 @@ from agentex.lib.types.fastacp import TemporalACPConfig # Create the ACP server acp = FastACP.create( - acp_type="agentic", + acp_type="async", config=TemporalACPConfig( type="temporal", temporal_address=os.getenv("TEMPORAL_ADDRESS", "localhost:7233") @@ -320,7 +320,7 @@ if __name__ == "__main__": ## Next Steps -- **Getting started?** Learn about [Base Agentic ACP](base.md) first +- **Getting started?** Learn about [Base Async ACP](base.md) first - **Need to migrate?** Check the [Migration Guide](../../concepts/migration_guide.md) - **New to Agentex?** Follow the [Quick Start Guide on GitHub](https://github.com/scaleapi/scale-agentex#quick-start) - **Ready to build?** Check out [Temporal Tutorials on GitHub](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/100_temporal) diff --git a/agentex/docs/docs/acp/best-practices.md b/agentex/docs/docs/acp/best-practices.md index b1656a8..bff3b3b 100644 --- a/agentex/docs/docs/acp/best-practices.md +++ b/agentex/docs/docs/acp/best-practices.md @@ -14,7 +14,7 @@ Guidelines for building robust, maintainable, and secure agents with ACP. - **Async operations** - Use `await` for all ADK operations - **Efficient state access** - Batch state operations when possible -- **Resource cleanup** - Always clean up resources in Agentic ACP +- **Resource cleanup** - Always clean up resources in Async ACP - **Caching** - Cache expensive operations when appropriate ## Security diff --git a/agentex/docs/docs/acp/overview.md b/agentex/docs/docs/acp/overview.md index 9d68b74..55fe3c2 100644 --- a/agentex/docs/docs/acp/overview.md +++ b/agentex/docs/docs/acp/overview.md @@ -19,7 +19,7 @@ Think of ACP as the "language" that agents and clients use to understand each ot Agentex supports two distinct ACP types, each designed for different use cases: -| Feature | Sync ACP | Agentic ACP | +| Feature | Sync ACP | Async ACP | |---------|----------|-------------| | **Best for** | Chat bots, simple Q&A | Complex workflows, stateful apps | | **Handlers** | 1 method (`on_message_send`) | 3 methods (`on_task_create`, `on_task_event_send`, `on_task_cancel`) | @@ -37,7 +37,7 @@ Agentex supports two distinct ACP types, each designed for different use cases: - ✅ Quick responses - Fast, lightweight operations - ✅ Chat-like interfaces - Traditional conversational AI -**Use Agentic ACP when:** +**Use Async ACP when:** - ✅ Complex workflows - Multi-step processes with state - ✅ Persistent memory - Need to remember context across interactions @@ -55,17 +55,17 @@ Perfect for simple chat bots and Q&A agents. Get started with the easiest approa **[→ Read Sync ACP Guide](sync.md)** -### Agentic ACP +### Async ACP Powerful event-driven approach for complex agents. Choose between Base (learning/development) or Temporal (production). -**[→ Read Agentic ACP Overview](agentic/overview.md)** +**[→ Read Async ACP Overview](async/overview.md)** --- ## Additional Resources -- **[Migration Guide](../concepts/migration_guide.md)** - Upgrade from Sync to Agentic, or Base to Temporal +- **[Migration Guide](../concepts/migration_guide.md)** - Upgrade from Sync to Async, or Base to Temporal - **[Best Practices](best-practices.md)** - Guidelines for performance, security, and maintainability --- diff --git a/agentex/docs/docs/acp/sync.md b/agentex/docs/docs/acp/sync.md index 1565477..ee98085 100644 --- a/agentex/docs/docs/acp/sync.md +++ b/agentex/docs/docs/acp/sync.md @@ -91,7 +91,7 @@ The `@acp.on_message_send` handler receives: ### Not Ideal For: -❌ **Multi-step workflows** - Use Agentic ACP +❌ **Multi-step workflows** - Use Async ACP ❌ **Complex state management** - Limited state tracking ❌ **Resource coordination** - No initialization/cleanup hooks ❌ **Long-running processes** - Better suited for quick responses @@ -101,13 +101,13 @@ The `@acp.on_message_send` handler receives: - **Keep handlers simple** - Sync ACP is for straightforward logic - **Handle errors gracefully** - Return error messages to users - **Use optional state sparingly** - For simple session data only -- **Consider migration** - Switch to Agentic ACP as complexity grows +- **Consider migration** - Switch to Async ACP as complexity grows --- ## Next Steps -- **Need more complexity?** Learn about [Agentic ACP](agentic/overview.md) +- **Need more complexity?** Learn about [Async ACP](agentic/overview.md) - **Ready to upgrade?** See the [Migration Guide](../concepts/migration_guide.md) - **New to Agentex?** Follow the [Quick Start Guide on GitHub](https://github.com/scaleapi/scale-agentex#quick-start) - **Ready to build?** Check out [Sync ACP Tutorials on GitHub](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync) diff --git a/agentex/docs/docs/api/types.md b/agentex/docs/docs/api/types.md index 84f3e7d..072c0fa 100644 --- a/agentex/docs/docs/api/types.md +++ b/agentex/docs/docs/api/types.md @@ -128,6 +128,6 @@ For practical usage examples and patterns, see: - [TaskMessage Concepts](../concepts/task_message.md) - Working with messages - [Streaming Concepts](../concepts/streaming.md) - Real-time message streaming - [State Concepts](../concepts/state.md) - Managing persistent state -- [Agent-to-Client Protocol (ACP)](../acp/overview.md) - Sync and Agentic ACP handler parameters explained +- [Agent-to-Client Protocol (ACP)](../acp/overview.md) - Sync and Async ACP handler parameters explained \ No newline at end of file diff --git a/agentex/docs/docs/concepts/agents.md b/agentex/docs/docs/concepts/agents.md index 80b2aac..a25059c 100644 --- a/agentex/docs/docs/concepts/agents.md +++ b/agentex/docs/docs/concepts/agents.md @@ -67,7 +67,7 @@ The way you implement agents depends on which ACP (Agent-to-Client Protocol) typ **Sync ACP** agents are the simplest form - just a single function that processes messages and returns responses. -**Agentic ACP** agents have multiple handler functions that manage the complete interaction lifecycle. +**Async ACP** agents have multiple handler functions that manage the complete interaction lifecycle. ## Agent Relationships @@ -163,7 +163,7 @@ async def handle_message_send(params: SendMessageParams): content="Hello from the agent!" ) -# Agentic ACP - Create messages explicitly +# Async ACP - Create messages explicitly @acp.on_task_event_send async def handle_event_send(params: SendEventParams): await adk.messages.create( diff --git a/agentex/docs/docs/concepts/callouts/events_vs_messages.md b/agentex/docs/docs/concepts/callouts/events_vs_messages.md index 444dc94..680f502 100644 --- a/agentex/docs/docs/concepts/callouts/events_vs_messages.md +++ b/agentex/docs/docs/concepts/callouts/events_vs_messages.md @@ -1,7 +1,7 @@ # 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. +!!! danger "Critical for Async ACP" + **Events and TaskMessages serve different purposes and are stored in separate database tables.** This distinction is fundamental to understanding how Async ACP works. ## The Core Distinction diff --git a/agentex/docs/docs/concepts/callouts/overview.md b/agentex/docs/docs/concepts/callouts/overview.md index 1852040..29557fe 100644 --- a/agentex/docs/docs/concepts/callouts/overview.md +++ b/agentex/docs/docs/concepts/callouts/overview.md @@ -17,7 +17,7 @@ This section covers critical implementation details that require special attenti **Critical for LLM integrations** - The important distinction between Agentex TaskMessages and LLM-compatible message formats, and when conversion is needed. ### [Events vs Messages](events_vs_messages.md) -**Critical for Agentic ACP** - Understanding that events are ephemeral notifications, not persistent objects like TaskMessages. +**Critical for Async ACP** - Understanding that events are ephemeral notifications, not persistent objects like TaskMessages. ### [Task/Agent Scoped State](state_management.md) **Critical for multi-agent systems** - How state is scoped per (task_id, agent_id) and why this design enables parallel agent execution. diff --git a/agentex/docs/docs/concepts/callouts/race_conditions.md b/agentex/docs/docs/concepts/callouts/race_conditions.md index 113e151..05de5d2 100644 --- a/agentex/docs/docs/concepts/callouts/race_conditions.md +++ b/agentex/docs/docs/concepts/callouts/race_conditions.md @@ -1,4 +1,4 @@ -# Race Conditions in Agentic ACP +# Race Conditions in Async 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. @@ -9,7 +9,7 @@ In **all agentic ACP types**, multiple events can trigger concurrent processing, **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) +### ❌ What Happens with Race Conditions (All Async ACP) ```python # Any agentic ACP - Multiple events can trigger simultaneously @@ -229,7 +229,7 @@ async def on_task_event_send(self, params): ### 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: +For Base Async ACP, you can use **Agent Task Tracker cursors** to coordinate processing and reduce race conditions: ```python @acp.on_task_event_send @@ -320,7 +320,7 @@ async def cursor_batch_handler(params: SendEventParams): ``` !!! 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. + Agent Task Tracker cursors **reduce** race conditions but don't eliminate them entirely. For complete race condition prevention, use **Temporal Async ACP** which provides guaranteed sequential processing. ## Common Race Condition Scenarios diff --git a/agentex/docs/docs/concepts/callouts/streaming.md b/agentex/docs/docs/concepts/callouts/streaming.md index a298aa3..6b1ddfe 100644 --- a/agentex/docs/docs/concepts/callouts/streaming.md +++ b/agentex/docs/docs/concepts/callouts/streaming.md @@ -30,21 +30,21 @@ async def handle_message_send(params: SendMessageParams) -> AsyncGenerator[TaskM ) ``` -### Agentic ACP: Three Streaming Options +### Async 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. +Agentex uses a **pub/sub mechanism** in the server to handle Async 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: +The ADK provides two different streaming approaches for agents behind Async 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 +# Async ACP - Auto send handles all delta accumulation @acp.on_task_event_send async def handle_event_send(params: SendEventParams): # Echo user message @@ -80,7 +80,7 @@ async def handle_event_send(params: SendEventParams): **You get full control but handle everything manually:** ```python -# Agentic ACP - Non-auto send: YOU handle everything +# Async 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 diff --git a/agentex/docs/docs/concepts/events.md b/agentex/docs/docs/concepts/events.md index 5c6c78a..286cba3 100644 --- a/agentex/docs/docs/concepts/events.md +++ b/agentex/docs/docs/concepts/events.md @@ -1,9 +1,9 @@ # Events -Events are persistent database records that coordinate agent processing workflows in **Agentic ACP only**. They represent activity within tasks and enable event-driven architectures for building sophisticated, distributed agents. +Events are persistent database records that coordinate agent processing workflows in **Async ACP only**. They represent activity within tasks and enable event-driven architectures for building sophisticated, distributed agents. -!!! warning "Agentic ACP Only" - Events are **only available in Agentic ACP**, not Sync ACP. Sync ACP uses direct request/response patterns without events. +!!! warning "Async ACP Only" + Events are **only available in Async ACP**, not Sync ACP. Sync ACP uses direct request/response patterns without events. ## What Are Events? @@ -11,7 +11,7 @@ Events are **processing coordination objects** stored in the events database tab **Key Characteristics:** -- **Agentic ACP Only**: Not available in Sync ACP +- **Async ACP Only**: Not available in Sync ACP - **Persistent**: Stored permanently in the events database table - **Sequential**: Ordered by `sequence_id` for predictable processing - **Pre-saved**: Written to database **BEFORE** being delivered to agents via ACP @@ -31,7 +31,7 @@ class Event: ## When to Use Events -Use events in Agentic ACP when you need: +Use events in Async ACP when you need: - **Batch Processing**: Accumulate multiple events and process together - **Progress Tracking**: Track which events have been processed @@ -40,7 +40,7 @@ Use events in Agentic ACP when you need: ## Event Processing Flow -The event processing flow in Agentic ACP: +The event processing flow in Async ACP: 1. **User Action**: User sends message or performs action 2. **Event Created**: Event written to events table **BEFORE** ACP delivery @@ -122,7 +122,7 @@ async def handle_with_context(params: SendEventParams): ## Key Points -- **Agentic ACP Only**: Events are not available in Sync ACP +- **Async ACP Only**: Events are not available in Sync ACP - **Pre-stored**: Events written to database before ACP delivery - **Agent Creates Messages**: Agent developers create TaskMessages, not the server - **Cursor-Based**: Use with Agent Task Tracker for progress tracking diff --git a/agentex/docs/docs/concepts/migration_guide.md b/agentex/docs/docs/concepts/migration_guide.md index 86e6369..da88f15 100644 --- a/agentex/docs/docs/concepts/migration_guide.md +++ b/agentex/docs/docs/concepts/migration_guide.md @@ -6,11 +6,11 @@ This guide provides comprehensive, step-by-step instructions for migrating agent | From | To | When to Migrate | Complexity Increase | |------|----|-----------------|--------------------| -| **Sync ACP** (~30 lines) | **Base Agentic ACP** (~80 lines) | Need state management, lifecycle control | Medium | -| **Base Agentic ACP** (~80 lines) | **Temporal Agentic ACP** (~150+ lines) | Production reliability, enterprise scale | High | -| **Sync ACP** (~30 lines) | **Temporal Agentic ACP** (~150+ lines) | Direct to production (skip intermediate) | Very High | +| **Sync ACP** (~30 lines) | **Base Async ACP** (~80 lines) | Need state management, lifecycle control | Medium | +| **Base Async ACP** (~80 lines) | **Temporal Async ACP** (~150+ lines) | Production reliability, enterprise scale | High | +| **Sync ACP** (~30 lines) | **Temporal Async ACP** (~150+ lines) | Direct to production (skip intermediate) | Very High | -## Part 1: Sync ACP → Base Agentic ACP +## Part 1: Sync ACP → Base Async ACP ### When to Migrate @@ -49,7 +49,7 @@ async def handle_message_send(params: SendMessageParams): return TextContent(author=MessageAuthor.AGENT, content=response) ``` -**After (Base Agentic ACP)**: +**After (Base Async ACP)**: ```python from agentex.sdk.fastacp.fastacp import FastACP from agentex.types.acp import CreateTaskParams, SendEventParams, CancelTaskParams @@ -57,7 +57,7 @@ from agentex.types.task_messages import TextContent, MessageAuthor from agentex import adk acp = FastACP.create( - acp_type="agentic", + acp_type="async", acp_config=AgenticACPConfig(acp_type="base") ) @@ -136,7 +136,7 @@ async def generate_ai_response(user_input, context): return f"You said: {user_input}. Context: {len(context)} previous messages" ``` -**Migrated Base Agentic ACP (85 lines)**: +**Migrated Base Async ACP (85 lines)**: ```python from agentex.sdk.fastacp.fastacp import FastACP from agentex.types.acp import CreateTaskParams, SendEventParams, CancelTaskParams @@ -145,7 +145,7 @@ from agentex.types.fastacp import AgenticACPConfig from agentex import adk acp = FastACP.create( - acp_type="agentic", + acp_type="async", acp_config=AgenticACPConfig(acp_type="base") ) @@ -236,7 +236,7 @@ async def generate_ai_response(user_input, context, state=None): 4. **Lifecycle Control**: None → Explicit create/cancel handlers 5. **Enhanced Capabilities**: Can now track conversation state, send welcome messages, cleanup resources -## Part 2: Base Agentic ACP → Temporal Agentic ACP +## Part 2: Base Async ACP → Temporal Async ACP ### When to Migrate @@ -248,7 +248,7 @@ async def generate_ai_response(user_input, context, state=None): - ✅ **Complex state coordination** across multiple activities - ✅ **Guaranteed execution** with distributed processing -**Warning Signs** in your Base Agentic ACP: +**Warning Signs** in your Base Async ACP: - Manual retry logic becoming complex - State coordination across multiple async operations @@ -260,10 +260,10 @@ async def generate_ai_response(user_input, context, state=None): #### Step 1: Create Temporal Workflow Structure -**Before (Base Agentic ACP)**: +**Before (Base Async ACP)**: ```python acp = FastACP.create( - acp_type="agentic", + acp_type="async", acp_config=AgenticACPConfig(acp_type="base") ) @@ -278,13 +278,13 @@ async def handle_event_send(params: SendEventParams): await adk.messages.create(task_id=params.task.id, content=result) ``` -**After (Temporal Agentic ACP)**: +**After (Temporal Async ACP)**: ```python from temporalio import workflow, activity from agentex.core.temporal.workflow import AgentexWorkflow acp = FastACP.create( - acp_type="agentic", + acp_type="async", acp_config=AgenticACPConfig(acp_type="temporal") ) @@ -381,7 +381,7 @@ await workflow.execute_activity( ```python # Migration helper async def migrate_state(task_id: str, from_type: str, to_type: str): - if from_type == "sync" and to_type == "agentic": + if from_type == "sync" and to_type == "async": # Sync uses automatic state, Agentic needs explicit creation existing_messages = await adk.messages.list(task_id=task_id) initial_state = {"migrated_message_count": len(existing_messages)} diff --git a/agentex/docs/docs/concepts/streaming.md b/agentex/docs/docs/concepts/streaming.md index 8b5ff3d..a57491a 100644 --- a/agentex/docs/docs/concepts/streaming.md +++ b/agentex/docs/docs/concepts/streaming.md @@ -114,14 +114,14 @@ async def generate_streaming_response(input_text: str) -> AsyncGenerator[str, No yield chunk.choices[0].delta.content ``` -### Agentic ACP Streaming +### Async ACP Streaming For event-driven workflows: ```python @acp.on_task_event_send async def handle_event_send(params: SendEventParams): - """Streaming in Agentic ACP""" + """Streaming in Async ACP""" # Use ADK streaming module for manual control stream = adk.streaming.create_stream( diff --git a/agentex/docs/docs/concepts/task.md b/agentex/docs/docs/concepts/task.md index 2fcfeb2..6311f02 100644 --- a/agentex/docs/docs/concepts/task.md +++ b/agentex/docs/docs/concepts/task.md @@ -46,9 +46,9 @@ async def handle_message_send(params: SendMessageParams): ) ``` -### Agentic ACP - Full Lifecycle Management +### Async ACP - Full Lifecycle Management -**Agentic ACP** gives you complete control over task lifecycle with event-driven handlers. This is for complex workflows and stateful interactions. +**Async ACP** gives you complete control over task lifecycle with event-driven handlers. This is for complex workflows and stateful interactions. **Key Characteristics:** @@ -95,9 +95,9 @@ async def handle_message_send(params: SendMessageParams): # 3. Task Completion - Unnecessary in Sync ACP ``` -### Agentic ACP Lifecycle +### Async ACP Lifecycle -In Agentic ACP, you control the entire task lifecycle: +In Async ACP, you control the entire task lifecycle: ```python # 1. Task Creation - Initialize whatever you need @@ -128,7 +128,7 @@ Task IDs work the same way in both ACP types - they're globally unique identifie async def handle_message_send(params: SendMessageParams): task_id = params.task.id # Available in message params -# Agentic ACP +# Async ACP @acp.on_task_create async def handle_task_create(params: CreateTaskParams): task_id = params.task.id # Available in all handlers @@ -177,7 +177,7 @@ This flat structure allows: - **Notification System**: Events signal that new messages have arrived, like "new mail in your mailbox" - **Content Wrapper**: Events contain `TaskMessageContent` but are not the source of truth - **Ephemeral**: Events are notifications, not stored entities you query later -- **Triggering Mechanism**: In Agentic ACP, events trigger your `@acp.on_task_event_send` handlers +- **Triggering Mechanism**: In Async ACP, events trigger your `@acp.on_task_event_send` handlers #### Processing Strategies: diff --git a/agentex/docs/docs/concepts/task_message.md b/agentex/docs/docs/concepts/task_message.md index c95d4bd..bedf650 100644 --- a/agentex/docs/docs/concepts/task_message.md +++ b/agentex/docs/docs/concepts/task_message.md @@ -203,7 +203,7 @@ await adk.messages.create( If you want to **send a message directly to an agent** (not just create it in the task ledger), use the appropriate ACP function: - **Sync ACP**: Use `adk.acp.send_message()` to trigger the agent's `@acp.on_message_send` handler - - **Agentic ACP**: Use `adk.acp.send_event()` to trigger the agent's `@acp.on_task_event_send` handler + - **Async ACP**: Use `adk.acp.send_event()` to trigger the agent's `@acp.on_task_event_send` handler Simply using `adk.messages.create()` only adds the message to the task ledger - it doesn't notify or trigger the agent to process it. diff --git a/agentex/docs/docs/guides/openai_temporal_integration.md b/agentex/docs/docs/guides/openai_temporal_integration.md index b777e12..e6637d3 100644 --- a/agentex/docs/docs/guides/openai_temporal_integration.md +++ b/agentex/docs/docs/guides/openai_temporal_integration.md @@ -54,7 +54,7 @@ from agentex.lib.plugins.openai_agents import OpenAIAgentsPlugin import os acp = FastACP.create( - acp_type="agentic", + acp_type="async", config=TemporalACPConfig( type="temporal", temporal_address=os.getenv("TEMPORAL_ADDRESS", "localhost:7233"), diff --git a/agentex/docs/docs/local_development/notebooks.md b/agentex/docs/docs/local_development/notebooks.md index 552eb0f..aabe483 100644 --- a/agentex/docs/docs/local_development/notebooks.md +++ b/agentex/docs/docs/local_development/notebooks.md @@ -1,6 +1,6 @@ # Jupyter Notebooks for Agent Development -Jupyter Notebooks provide an excellent environment for developing and testing AgentEx agents. This guide will walk you through how to use notebooks effectively for both **Sync ACP** and **Agentic ACP** agents. +Jupyter Notebooks provide an excellent environment for developing and testing AgentEx agents. This guide will walk you through how to use notebooks effectively for both **Sync ACP** and **Async ACP** agents. ## Prerequisites @@ -103,11 +103,11 @@ for agent_rpc_response_chunk in client.agents.send_message_stream( print(f"Streaming update: {task_message_update}") ``` -## Agentic ACP Agents +## Async ACP Agents ### Lifecycle Overview -Agentic ACP agents work asynchronously: +Async ACP agents work asynchronously: - **Send events** → **Agent processes when ready** → **Subscribe to responses** - Events are like **mobile phone notifications** - asynchronous and non-blocking @@ -129,10 +129,10 @@ AGENT_NAME = "your-agentic-agent-name" ### Step 1: Create a Task (Required) -For agentic agents, you **must** create a task first: +For async agents, you **must** create a task first: ```python -# Create a new task (REQUIRED for agentic agents) +# Create a new task (REQUIRED for async agents) rpc_response = client.agents.create_task( agent_name=AGENT_NAME, params={ @@ -169,7 +169,7 @@ print(f"Sent event: {event.id}") ### Step 3: Subscribe to Async Responses -Since agentic agents work asynchronously, you need to subscribe to responses: +Since async agents work asynchronously, you need to subscribe to responses: ```python from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages @@ -234,7 +234,7 @@ When using `cancel_task`: ### Working with Different Content Types -Both sync and agentic agents support various content types: +Both sync and async agents support various content types: ```python # Text content (most common) diff --git a/agentex/docs/docs/tutorials.md b/agentex/docs/docs/tutorials.md index 0f63b36..d38f90f 100644 --- a/agentex/docs/docs/tutorials.md +++ b/agentex/docs/docs/tutorials.md @@ -14,11 +14,11 @@ Perfect for learning basic agent patterns with simple request-response interacti - **[Multiturn](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/010_multiturn)** - Add conversation history and memory - **[Streaming](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/020_streaming)** - Stream real-time responses to clients -### Agentic ACP - Base (Learning & Development) +### Async ACP - Base (Learning & Development) Learn stateful workflows with full lifecycle control. Great for development and understanding advanced patterns. -- **[Hello Agentic ACP](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/00_base/000_hello_acp)** - Three-handler pattern fundamentals +- **[Hello Async ACP](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/00_base/000_hello_acp)** - Three-handler pattern fundamentals - **[Multiturn](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/00_base/010_multiturn)** - Build stateful conversations - **[Streaming](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/00_base/020_streaming)** - Stream responses in complex workflows - **[Tracing](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/00_base/030_tracing)** - Add observability and debugging @@ -26,7 +26,7 @@ Learn stateful workflows with full lifecycle control. Great for development and - **[Batch Events](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/00_base/080_batch_events)** - Handle multiple events efficiently - **[Multi-Agent Assembly Line](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_agentic/00_base/090_multi_agent_non_temporal)** - Coordinate multiple agents without Temporal -### Agentic ACP - Temporal (Production) +### Async ACP - Temporal (Production) Enterprise-ready patterns with durable execution. For production deployments requiring reliability and fault tolerance. diff --git a/agentex/docs/mkdocs.yml b/agentex/docs/mkdocs.yml index d882f35..fd84d28 100644 --- a/agentex/docs/mkdocs.yml +++ b/agentex/docs/mkdocs.yml @@ -52,10 +52,10 @@ nav: - Agent Types: - Agent to Client Protocol (ACP): agent_types/overview.md - Sync ACP: agent_types/sync.md - - Agentic ACP: - - Overview: agent_types/agentic/overview.md - - Base Implementation: agent_types/agentic/base.md - - Temporal Implementation: agent_types/agentic/temporal.md + - Async ACP: + - Overview: agent_types/async/overview.md + - Base Implementation: agent_types/async/base.md + - Temporal Implementation: agent_types/async/temporal.md - Migration Guide: agent_types/agent_type_migration_guide.md - Core Concepts: - Agents: concepts/agents.md diff --git a/agentex/src/adapters/orm.py b/agentex/src/adapters/orm.py index ed4eed4..3eb45e9 100644 --- a/agentex/src/adapters/orm.py +++ b/agentex/src/adapters/orm.py @@ -33,7 +33,8 @@ class AgentORM(BaseORM): status = Column(SQLAlchemyEnum(AgentStatus), nullable=False) status_reason = Column(Text, nullable=True) acp_url = Column(String, nullable=True) # URL of the agent's ACP server - acp_type = Column(String, nullable=False, server_default="agentic") + # TODO: make this a SQLAlchemyEnum rather than a string + acp_type = Column(String, nullable=False, server_default="async") created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() diff --git a/agentex/src/api/schemas/agents.py b/agentex/src/api/schemas/agents.py index 7f2f481..73f250e 100644 --- a/agentex/src/api/schemas/agents.py +++ b/agentex/src/api/schemas/agents.py @@ -16,7 +16,9 @@ class AgentStatus(str, Enum): class ACPType(str, Enum): SYNC = "sync" - AGENTIC = "agentic" + ASYNC = "async" + + AGENTIC = "agentic" # deprecated: use ASYNC instead class AgentRPCMethod(str, Enum): @@ -36,7 +38,7 @@ class Agent(BaseModel): ) acp_type: ACPType = Field( ..., - description="The type of the ACP Server (Either sync or agentic)", + description="The type of the ACP Server (Either sync or async)", ) status_reason: str | None = Field( None, description="The reason for the status of the action." diff --git a/agentex/src/domain/entities/agents.py b/agentex/src/domain/entities/agents.py index bcb62e4..9eb7020 100644 --- a/agentex/src/domain/entities/agents.py +++ b/agentex/src/domain/entities/agents.py @@ -16,7 +16,9 @@ class AgentStatus(str, Enum): class ACPType(str, Enum): SYNC = "sync" - AGENTIC = "agentic" + ASYNC = "async" + + AGENTIC = "agentic" # deprecated: use ASYNC instead class AgentEntity(BaseModel): @@ -28,8 +30,8 @@ class AgentEntity(BaseModel): name: str = Field(..., description="The unique name of the agent.") description: str = Field(..., description="The description of the action.") acp_type: ACPType = Field( - ACPType.AGENTIC, - description="The type of the ACP Server (Either sync or agentic)", + ACPType.ASYNC, + description="The type of the ACP Server (Either sync or async)", ) status: AgentStatus = Field( AgentStatus.UNKNOWN, diff --git a/agentex/src/domain/entities/agents_rpc.py b/agentex/src/domain/entities/agents_rpc.py index 3a14c15..571ba10 100644 --- a/agentex/src/domain/entities/agents_rpc.py +++ b/agentex/src/domain/entities/agents_rpc.py @@ -92,6 +92,11 @@ class CancelTaskParams(BaseModel): AgentRPCMethod.TASK_CANCEL, AgentRPCMethod.EVENT_SEND, ], + ACPType.ASYNC: [ + AgentRPCMethod.TASK_CREATE, + AgentRPCMethod.TASK_CANCEL, + AgentRPCMethod.EVENT_SEND, + ], } diff --git a/agentex/src/domain/use_cases/agents_acp_use_case.py b/agentex/src/domain/use_cases/agents_acp_use_case.py index b57b9ef..3f3c5a9 100644 --- a/agentex/src/domain/use_cases/agents_acp_use_case.py +++ b/agentex/src/domain/use_cases/agents_acp_use_case.py @@ -355,7 +355,7 @@ async def _handle_task_create( agent=agent, task_name=params.name, task_params=params.params ) - if agent.acp_type == ACPType.AGENTIC: + if agent.acp_type in [ACPType.AGENTIC, ACPType.ASYNC]: await self.task_service.forward_task_to_acp( agent=agent, task=task, diff --git a/agentex/src/domain/use_cases/agents_use_case.py b/agentex/src/domain/use_cases/agents_use_case.py index 3ce98d7..061511b 100644 --- a/agentex/src/domain/use_cases/agents_use_case.py +++ b/agentex/src/domain/use_cases/agents_use_case.py @@ -30,7 +30,7 @@ async def register_agent( description: str, acp_url: str, agent_id: str | None = None, - acp_type: ACPType = ACPType.AGENTIC, + acp_type: ACPType = ACPType.ASYNC, registration_metadata: dict[str, Any] | None = None, ) -> AgentEntity: # If an agent_id is passed, then the agent expects that it is already in the db diff --git a/agentex/tests/integration/api/agents/test_acp_type_backwards_compatibility.py b/agentex/tests/integration/api/agents/test_acp_type_backwards_compatibility.py new file mode 100644 index 0000000..cd2fbae --- /dev/null +++ b/agentex/tests/integration/api/agents/test_acp_type_backwards_compatibility.py @@ -0,0 +1,286 @@ +""" +Integration tests for ACPType backwards compatibility. +Tests the full HTTP API with legacy "agentic" agents. +""" + +import pytest + + +@pytest.mark.integration +class TestACPTypeBackwardsCompatibilityIntegration: + """Integration tests ensuring legacy AGENTIC agents work via API""" + + @pytest.mark.asyncio + async def test_register_agent_with_agentic_type(self, isolated_client): + """Test that we can register an agent with acp_type='agentic'""" + response = await isolated_client.post( + "/agents/register", + json={ + "name": "legacy-agentic-agent", + "description": "A legacy agent using agentic type", + "acp_url": "http://legacy-acp-server:8000", + "acp_type": "agentic", + }, + ) + + assert response.status_code == 200 + agent_data = response.json() + assert agent_data["name"] == "legacy-agentic-agent" + assert agent_data["acp_type"] == "agentic" + assert agent_data["id"] is not None + assert "agent_api_key" in agent_data + + @pytest.mark.asyncio + async def test_register_agent_with_async_type(self, isolated_client): + """Test that we can register an agent with acp_type='async'""" + response = await isolated_client.post( + "/agents/register", + json={ + "name": "new-async-agent", + "description": "A new agent using async type", + "acp_url": "http://async-acp-server:8000", + "acp_type": "async", + }, + ) + + assert response.status_code == 200 + agent_data = response.json() + assert agent_data["name"] == "new-async-agent" + assert agent_data["acp_type"] == "async" + assert agent_data["id"] is not None + assert "agent_api_key" in agent_data + + @pytest.mark.asyncio + async def test_retrieve_agentic_agent_preserves_type(self, isolated_client): + """Test that AGENTIC agents return 'agentic' in API responses (not converted)""" + # Register an agentic agent + register_response = await isolated_client.post( + "/agents/register", + json={ + "name": "test-agentic-retrieval", + "description": "Test retrieval of agentic type", + "acp_url": "http://test-acp-server:8000", + "acp_type": "agentic", + }, + ) + assert register_response.status_code == 200 + agent_id = register_response.json()["id"] + + # Retrieve by ID + get_response = await isolated_client.get(f"/agents/{agent_id}") + assert get_response.status_code == 200 + agent_data = get_response.json() + assert ( + agent_data["acp_type"] == "agentic" + ), "API should return 'agentic' for legacy agents, not convert to 'async'" + + # Retrieve by name + get_by_name_response = await isolated_client.get( + "/agents/name/test-agentic-retrieval" + ) + assert get_by_name_response.status_code == 200 + agent_by_name = get_by_name_response.json() + assert agent_by_name["acp_type"] == "agentic" + + @pytest.mark.asyncio + async def test_list_agents_shows_both_agentic_and_async(self, isolated_client): + """Test that listing agents shows both agentic and async types correctly""" + # Register an agentic agent + await isolated_client.post( + "/agents/register", + json={ + "name": "list-test-agentic", + "description": "Agentic agent for list test", + "acp_url": "http://test1:8000", + "acp_type": "agentic", + }, + ) + + # Register an async agent + await isolated_client.post( + "/agents/register", + json={ + "name": "list-test-async", + "description": "Async agent for list test", + "acp_url": "http://test2:8000", + "acp_type": "async", + }, + ) + + # List all agents + list_response = await isolated_client.get("/agents") + assert list_response.status_code == 200 + agents = list_response.json() + + # Find our test agents + agentic_agent = next( + (a for a in agents if a["name"] == "list-test-agentic"), None + ) + async_agent = next((a for a in agents if a["name"] == "list-test-async"), None) + + assert agentic_agent is not None + assert agentic_agent["acp_type"] == "agentic" + + assert async_agent is not None + assert async_agent["acp_type"] == "async" + + @pytest.mark.asyncio + async def test_update_agent_from_agentic_to_async(self, isolated_client): + """Test that we can update an agent from agentic to async type""" + # Register an agentic agent + register_response = await isolated_client.post( + "/agents/register", + json={ + "name": "upgrade-test-agent", + "description": "Agent to be upgraded", + "acp_url": "http://test:8000", + "acp_type": "agentic", + }, + ) + assert register_response.status_code == 200 + agent_id = register_response.json()["id"] + + # Update to async type + update_response = await isolated_client.post( + "/agents/register", + json={ + "agent_id": agent_id, + "name": "upgrade-test-agent", + "description": "Agent upgraded to async", + "acp_url": "http://test:8000", + "acp_type": "async", + }, + ) + assert update_response.status_code == 200 + updated_agent = update_response.json() + assert updated_agent["id"] == agent_id + assert updated_agent["acp_type"] == "async" + + # Verify via GET + get_response = await isolated_client.get(f"/agents/{agent_id}") + assert get_response.status_code == 200 + assert get_response.json()["acp_type"] == "async" + + @pytest.mark.asyncio + async def test_update_agent_from_async_to_agentic(self, isolated_client): + """Test that we can update an agent from async to agentic type (for flexibility)""" + # Register an async agent + register_response = await isolated_client.post( + "/agents/register", + json={ + "name": "downgrade-test-agent", + "description": "Agent to be changed to agentic", + "acp_url": "http://test:8000", + "acp_type": "async", + }, + ) + assert register_response.status_code == 200 + agent_id = register_response.json()["id"] + + # Update to agentic type + update_response = await isolated_client.post( + "/agents/register", + json={ + "agent_id": agent_id, + "name": "downgrade-test-agent", + "description": "Agent changed to agentic", + "acp_url": "http://test:8000", + "acp_type": "agentic", + }, + ) + assert update_response.status_code == 200 + updated_agent = update_response.json() + assert updated_agent["id"] == agent_id + assert updated_agent["acp_type"] == "agentic" + + @pytest.mark.asyncio + async def test_invalid_acp_type_rejected(self, isolated_client): + """Test that invalid acp_type values are rejected""" + response = await isolated_client.post( + "/agents/register", + json={ + "name": "invalid-type-agent", + "description": "Agent with invalid type", + "acp_url": "http://test:8000", + "acp_type": "invalid_type", + }, + ) + + # Should return 422 Unprocessable Entity for invalid enum value + assert response.status_code == 422 + + @pytest.mark.asyncio + async def test_agentic_agent_can_create_tasks( + self, isolated_client, isolated_agent_repo + ): + """Test that agentic agents can create tasks via RPC""" + # Register an agentic agent + register_response = await isolated_client.post( + "/agents/register", + json={ + "name": "agentic-task-creator", + "description": "Agentic agent for task creation test", + "acp_url": "http://test:8000", + "acp_type": "agentic", + }, + ) + assert register_response.status_code == 200 + agent_id = register_response.json()["id"] + + # Try to create a task via RPC + rpc_response = await isolated_client.post( + f"/agents/{agent_id}/rpc", + json={ + "jsonrpc": "2.0", + "method": "task/create", + "params": { + "name": "test-task-from-agentic", + "params": {"test": "value"}, + }, + "id": "test-1", + }, + ) + + # Should succeed (agentic agents support task/create) + assert rpc_response.status_code == 200 + rpc_data = rpc_response.json() + assert rpc_data["error"] is None + assert rpc_data["result"]["name"] == "test-task-from-agentic" + + @pytest.mark.asyncio + async def test_async_agent_can_create_tasks( + self, isolated_client, isolated_agent_repo + ): + """Test that async agents can create tasks via RPC (same as agentic)""" + # Register an async agent + register_response = await isolated_client.post( + "/agents/register", + json={ + "name": "async-task-creator", + "description": "Async agent for task creation test", + "acp_url": "http://test:8000", + "acp_type": "async", + }, + ) + assert register_response.status_code == 200 + agent_id = register_response.json()["id"] + + # Try to create a task via RPC + rpc_response = await isolated_client.post( + f"/agents/{agent_id}/rpc", + json={ + "jsonrpc": "2.0", + "method": "task/create", + "params": { + "name": "test-task-from-async", + "params": {"test": "value"}, + }, + "id": "test-2", + }, + ) + + # Should succeed (async agents support task/create, same as agentic) + assert rpc_response.status_code == 200 + rpc_data = rpc_response.json() + assert rpc_data["error"] is None + assert rpc_data["result"]["name"] == "test-task-from-async" diff --git a/agentex/tests/unit/repositories/test_agent_api_key_repository.py b/agentex/tests/unit/repositories/test_agent_api_key_repository.py index 1a2ef6a..04d465d 100644 --- a/agentex/tests/unit/repositories/test_agent_api_key_repository.py +++ b/agentex/tests/unit/repositories/test_agent_api_key_repository.py @@ -73,7 +73,7 @@ async def test_agent_api_key_repository_crud_operations( docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) created_agent = await agent_repo.create(agent) diff --git a/agentex/tests/unit/repositories/test_agent_repository.py b/agentex/tests/unit/repositories/test_agent_repository.py index 2076329..a361be3 100644 --- a/agentex/tests/unit/repositories/test_agent_repository.py +++ b/agentex/tests/unit/repositories/test_agent_repository.py @@ -64,7 +64,7 @@ async def test_agent_repository_crud_operations(postgres_url, isolated_test_sche description="A test agent for unit testing", docker_image="test/agent:latest", status=AgentStatus.READY, - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, acp_url="http://localhost:8000/acp", ) diff --git a/agentex/tests/unit/repositories/test_agent_task_tracker_repository.py b/agentex/tests/unit/repositories/test_agent_task_tracker_repository.py index 8cdaea6..a7a3951 100644 --- a/agentex/tests/unit/repositories/test_agent_task_tracker_repository.py +++ b/agentex/tests/unit/repositories/test_agent_task_tracker_repository.py @@ -68,7 +68,7 @@ async def test_agent_task_tracker_repository_crud_operations(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) created_agent = await agent_repo.create(agent) diff --git a/agentex/tests/unit/repositories/test_event_repository.py b/agentex/tests/unit/repositories/test_event_repository.py index 2a5deb4..08aa7b9 100644 --- a/agentex/tests/unit/repositories/test_event_repository.py +++ b/agentex/tests/unit/repositories/test_event_repository.py @@ -66,7 +66,7 @@ async def test_event_repository_crud_operations(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) created_agent = await agent_repo.create(agent) diff --git a/agentex/tests/unit/repositories/test_task_repository.py b/agentex/tests/unit/repositories/test_task_repository.py index fffd2d1..32bc74d 100644 --- a/agentex/tests/unit/repositories/test_task_repository.py +++ b/agentex/tests/unit/repositories/test_task_repository.py @@ -71,7 +71,7 @@ async def test_task_repository_crud_operations(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) created_agent = await agent_repo.create(agent) @@ -219,7 +219,7 @@ async def test_task_repository_params_support(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) created_agent = await agent_repo.create(agent) @@ -357,7 +357,7 @@ async def test_task_repository_task_metadata_support(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) created_agent = await agent_repo.create(agent) @@ -548,7 +548,7 @@ async def test_task_repository_null_task_metadata_handling(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) created_agent = await agent_repo.create(agent) @@ -694,7 +694,7 @@ async def test_list_with_join_includes_task_metadata(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) await agent_repo.create(agent_1) @@ -705,7 +705,7 @@ async def test_list_with_join_includes_task_metadata(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) await agent_repo.create(agent_2) @@ -897,7 +897,7 @@ async def test_list_with_join(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) await agent_repo.create(agent_1) @@ -908,7 +908,7 @@ async def test_list_with_join(postgres_url): docker_image="test/agent:latest", status=AgentStatus.READY, acp_url="http://localhost:8000/acp", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, ) await agent_repo.create(agent_2) diff --git a/agentex/tests/unit/services/test_agent_acp_service.py b/agentex/tests/unit/services/test_agent_acp_service.py index 42bfad4..e226e62 100644 --- a/agentex/tests/unit/services/test_agent_acp_service.py +++ b/agentex/tests/unit/services/test_agent_acp_service.py @@ -61,7 +61,7 @@ def sample_agent(): id=str(uuid4()), name="test-agent", description="A test agent for ACP service testing", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, status=AgentStatus.READY, acp_url="http://test-acp.example.com", ) diff --git a/agentex/tests/unit/services/test_task_service.py b/agentex/tests/unit/services/test_task_service.py index beea22f..b1a225d 100644 --- a/agentex/tests/unit/services/test_task_service.py +++ b/agentex/tests/unit/services/test_task_service.py @@ -95,7 +95,7 @@ def sample_agent(): name="test-agent", description="A test agent for unit testing", status=AgentStatus.READY, - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, acp_url="http://test-acp.example.com", ) diff --git a/agentex/tests/unit/use_cases/test_acp_type_backwards_compatibility.py b/agentex/tests/unit/use_cases/test_acp_type_backwards_compatibility.py new file mode 100644 index 0000000..fac43d5 --- /dev/null +++ b/agentex/tests/unit/use_cases/test_acp_type_backwards_compatibility.py @@ -0,0 +1,342 @@ +""" +Backwards compatibility tests for ACPType.AGENTIC. +Ensures legacy "agentic" agents continue to work alongside new "async" agents. +""" + +from unittest.mock import AsyncMock +from uuid import uuid4 + +import pytest +from src.domain.entities.agents import ACPType, AgentEntity, AgentStatus +from src.domain.entities.agents_rpc import ( + ACP_TYPE_TO_ALLOWED_RPC_METHODS, + AgentRPCMethod, +) +from src.domain.entities.tasks import TaskEntity, TaskStatus +from src.domain.repositories.agent_repository import AgentRepository +from src.domain.repositories.event_repository import EventRepository +from src.domain.repositories.task_repository import TaskRepository +from src.domain.repositories.task_state_repository import TaskStateRepository +from src.domain.services.agent_acp_service import AgentACPService +from src.domain.services.task_service import AgentTaskService +from src.domain.use_cases.agents_acp_use_case import AgentsACPUseCase +from src.domain.use_cases.agents_use_case import AgentsUseCase + + +@pytest.mark.unit +class TestACPTypeBackwardsCompatibility: + """Test that legacy AGENTIC agents work identically to new ASYNC agents""" + + @pytest.mark.asyncio + async def test_both_agentic_and_async_have_same_allowed_methods(self): + """Verify AGENTIC and ASYNC have identical RPC method permissions""" + agentic_methods = set(ACP_TYPE_TO_ALLOWED_RPC_METHODS[ACPType.AGENTIC]) + async_methods = set(ACP_TYPE_TO_ALLOWED_RPC_METHODS[ACPType.ASYNC]) + + assert ( + agentic_methods == async_methods + ), "AGENTIC and ASYNC should have identical allowed RPC methods" + + # Verify they include the expected methods + expected_methods = { + AgentRPCMethod.TASK_CREATE, + AgentRPCMethod.TASK_CANCEL, + AgentRPCMethod.EVENT_SEND, + } + assert agentic_methods == expected_methods + assert async_methods == expected_methods + + @pytest.mark.asyncio + async def test_agentic_agent_in_allowed_methods_dictionary(self): + """Verify AGENTIC is still present in allowed methods dictionary""" + assert ACPType.AGENTIC in ACP_TYPE_TO_ALLOWED_RPC_METHODS + assert ACPType.ASYNC in ACP_TYPE_TO_ALLOWED_RPC_METHODS + assert ACPType.SYNC in ACP_TYPE_TO_ALLOWED_RPC_METHODS + + @pytest.mark.asyncio + async def test_validate_rpc_method_accepts_agentic_for_task_create(self): + """Verify AGENTIC agents can use task/create method""" + # Should not raise an error + AgentsACPUseCase._validate_rpc_method_for_acp_type( + ACPType.AGENTIC, AgentRPCMethod.TASK_CREATE + ) + + @pytest.mark.asyncio + async def test_validate_rpc_method_accepts_agentic_for_event_send(self): + """Verify AGENTIC agents can use event/send method""" + # Should not raise an error + AgentsACPUseCase._validate_rpc_method_for_acp_type( + ACPType.AGENTIC, AgentRPCMethod.EVENT_SEND + ) + + @pytest.mark.asyncio + async def test_validate_rpc_method_accepts_agentic_for_task_cancel(self): + """Verify AGENTIC agents can use task/cancel method""" + # Should not raise an error + AgentsACPUseCase._validate_rpc_method_for_acp_type( + ACPType.AGENTIC, AgentRPCMethod.TASK_CANCEL + ) + + @pytest.mark.asyncio + async def test_agentic_agent_forwards_task_to_acp(self): + """Verify AGENTIC agents forward tasks to ACP (not SYNC behavior)""" + # Setup mocks + acp_client = AsyncMock(spec=AgentACPService) + task_state_repo = AsyncMock(spec=TaskStateRepository) + task_repo = AsyncMock(spec=TaskRepository) + event_repo = AsyncMock(spec=EventRepository) + stream_repo = AsyncMock() + + task_service = AgentTaskService( + acp_client=acp_client, + task_state_repository=task_state_repo, + task_repository=task_repo, + event_repository=event_repo, + stream_repository=stream_repo, + ) + + # Create AGENTIC agent + agentic_agent = AgentEntity( + id=str(uuid4()), + name="legacy-agentic-agent", + description="A legacy agent with agentic type", + acp_type=ACPType.AGENTIC, + status=AgentStatus.READY, + acp_url="http://test-acp.example.com", + ) + + task = TaskEntity( + id=str(uuid4()), + name="test-task", + status=TaskStatus.RUNNING, + ) + + task_repo.create.return_value = task + acp_client.create_task.return_value = None + + # Execute + result = await task_service.create_task_and_forward_to_acp( + agent=agentic_agent, + task_name="test-task", + task_params={"test": "params"}, + ) + + # Verify task was forwarded to ACP (not skipped like SYNC agents) + acp_client.create_task.assert_called_once_with( + agent=agentic_agent, + task=task, + acp_url=agentic_agent.acp_url, + params={"test": "params"}, + ) + assert result == task + + @pytest.mark.asyncio + async def test_sync_agent_does_not_forward_task_to_acp(self): + """Verify SYNC agents skip ACP forwarding (baseline for comparison)""" + # Setup mocks + acp_client = AsyncMock(spec=AgentACPService) + task_state_repo = AsyncMock(spec=TaskStateRepository) + task_repo = AsyncMock(spec=TaskRepository) + event_repo = AsyncMock(spec=EventRepository) + stream_repo = AsyncMock() + + task_service = AgentTaskService( + acp_client=acp_client, + task_state_repository=task_state_repo, + task_repository=task_repo, + event_repository=event_repo, + stream_repository=stream_repo, + ) + + # Create SYNC agent + sync_agent = AgentEntity( + id=str(uuid4()), + name="sync-agent", + description="A sync agent", + acp_type=ACPType.SYNC, + status=AgentStatus.READY, + acp_url="http://test-acp.example.com", + ) + + task = TaskEntity( + id=str(uuid4()), + name="test-task", + status=TaskStatus.RUNNING, + ) + + task_repo.create.return_value = task + + # Execute + result = await task_service.create_task_and_forward_to_acp( + agent=sync_agent, + task_name="test-task", + task_params={"test": "params"}, + ) + + # Verify task was NOT forwarded to ACP (SYNC behavior) + acp_client.create_task.assert_not_called() + assert result == task + + @pytest.mark.asyncio + async def test_async_agent_forwards_task_to_acp(self): + """Verify ASYNC agents forward tasks to ACP (same as AGENTIC)""" + # Setup mocks + acp_client = AsyncMock(spec=AgentACPService) + task_state_repo = AsyncMock(spec=TaskStateRepository) + task_repo = AsyncMock(spec=TaskRepository) + event_repo = AsyncMock(spec=EventRepository) + stream_repo = AsyncMock() + + task_service = AgentTaskService( + acp_client=acp_client, + task_state_repository=task_state_repo, + task_repository=task_repo, + event_repository=event_repo, + stream_repository=stream_repo, + ) + + # Create ASYNC agent + async_agent = AgentEntity( + id=str(uuid4()), + name="async-agent", + description="A new async agent", + acp_type=ACPType.ASYNC, + status=AgentStatus.READY, + acp_url="http://test-acp.example.com", + ) + + task = TaskEntity( + id=str(uuid4()), + name="test-task", + status=TaskStatus.RUNNING, + ) + + task_repo.create.return_value = task + acp_client.create_task.return_value = None + + # Execute + result = await task_service.create_task_and_forward_to_acp( + agent=async_agent, + task_name="test-task", + task_params={"test": "params"}, + ) + + # Verify task was forwarded to ACP (same behavior as AGENTIC) + acp_client.create_task.assert_called_once_with( + agent=async_agent, + task=task, + acp_url=async_agent.acp_url, + params={"test": "params"}, + ) + assert result == task + + @pytest.mark.asyncio + async def test_register_agent_defaults_to_async_not_agentic(self): + """Verify new agent registrations default to ASYNC, not AGENTIC""" + # Setup mocks + agent_repo = AsyncMock(spec=AgentRepository) + + agents_use_case = AgentsUseCase(agent_repo=agent_repo) + + # Mock repository to return a new agent + expected_agent = AgentEntity( + id=str(uuid4()), + name="new-agent", + description="A new agent", + acp_type=ACPType.ASYNC, # Should be ASYNC by default + status=AgentStatus.READY, + acp_url="http://test-acp.example.com", + ) + agent_repo.get.side_effect = Exception("Agent not found") + agent_repo.create.return_value = expected_agent + + # Execute - without specifying acp_type, should default to ASYNC + await agents_use_case.register_agent( + name="new-agent", + description="A new agent", + acp_url="http://test-acp.example.com", + # acp_type not specified - should default to ASYNC + ) + + # Verify the call was made with ASYNC + agent_repo.create.assert_called_once() + call_args = agent_repo.create.call_args + created_agent = call_args[0][0] + assert created_agent.acp_type == ACPType.ASYNC + + @pytest.mark.asyncio + async def test_can_explicitly_register_agentic_agent(self): + """Verify we can still explicitly register AGENTIC agents for backwards compat""" + # Setup mocks + agent_repo = AsyncMock(spec=AgentRepository) + + agents_use_case = AgentsUseCase(agent_repo=agent_repo) + + # Mock repository to return an AGENTIC agent + expected_agent = AgentEntity( + id=str(uuid4()), + name="legacy-agent", + description="A legacy agent", + acp_type=ACPType.AGENTIC, + status=AgentStatus.READY, + acp_url="http://test-acp.example.com", + ) + agent_repo.get.side_effect = Exception("Agent not found") + agent_repo.create.return_value = expected_agent + + # Execute - explicitly specify AGENTIC + await agents_use_case.register_agent( + name="legacy-agent", + description="A legacy agent", + acp_url="http://test-acp.example.com", + acp_type=ACPType.AGENTIC, # Explicitly set to AGENTIC + ) + + # Verify the call was made with AGENTIC + agent_repo.create.assert_called_once() + call_args = agent_repo.create.call_args + created_agent = call_args[0][0] + assert created_agent.acp_type == ACPType.AGENTIC + + @pytest.mark.asyncio + async def test_agentic_and_async_agents_both_use_not_sync_logic(self): + """Verify conditional logic treats AGENTIC and ASYNC identically""" + # Create test agents + agentic_agent = AgentEntity( + id=str(uuid4()), + name="agentic-agent", + description="Legacy agentic agent", + acp_type=ACPType.AGENTIC, + status=AgentStatus.READY, + acp_url="http://test.com", + ) + + async_agent = AgentEntity( + id=str(uuid4()), + name="async-agent", + description="New async agent", + acp_type=ACPType.ASYNC, + status=AgentStatus.READY, + acp_url="http://test.com", + ) + + sync_agent = AgentEntity( + id=str(uuid4()), + name="sync-agent", + description="Sync agent", + acp_type=ACPType.SYNC, + status=AgentStatus.READY, + acp_url="http://test.com", + ) + + # Test the "!= SYNC" logic that's used in the codebase + assert agentic_agent.acp_type != ACPType.SYNC, "AGENTIC should not equal SYNC" + assert async_agent.acp_type != ACPType.SYNC, "ASYNC should not equal SYNC" + assert sync_agent.acp_type == ACPType.SYNC, "SYNC should equal SYNC" + + # Both AGENTIC and ASYNC should pass the same conditional checks + agentic_is_not_sync = agentic_agent.acp_type != ACPType.SYNC + async_is_not_sync = async_agent.acp_type != ACPType.SYNC + assert ( + agentic_is_not_sync == async_is_not_sync + ), "AGENTIC and ASYNC should have identical behavior in != SYNC checks" diff --git a/agentex/tests/unit/use_cases/test_agents_acp_use_case.py b/agentex/tests/unit/use_cases/test_agents_acp_use_case.py index a1c3e89..cccc11d 100644 --- a/agentex/tests/unit/use_cases/test_agents_acp_use_case.py +++ b/agentex/tests/unit/use_cases/test_agents_acp_use_case.py @@ -171,7 +171,7 @@ def sample_agent(): id=str(uuid4()), name="test-agent", description="A test agent for use case testing", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, status=AgentStatus.READY, acp_url="http://test-acp.example.com", ) diff --git a/agentex/tests/unit/use_cases/test_agents_api_keys_use_case.py b/agentex/tests/unit/use_cases/test_agents_api_keys_use_case.py index f84222c..b7a2f21 100644 --- a/agentex/tests/unit/use_cases/test_agents_api_keys_use_case.py +++ b/agentex/tests/unit/use_cases/test_agents_api_keys_use_case.py @@ -49,7 +49,7 @@ def sample_agent(): id=str(uuid4()), name="test-agent", description="A test agent for use case testing", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, status=AgentStatus.READY, acp_url="http://test-acp.example.com", ) diff --git a/agentex/tests/unit/use_cases/test_deployment_history_use_case.py b/agentex/tests/unit/use_cases/test_deployment_history_use_case.py index 758b750..5c26741 100644 --- a/agentex/tests/unit/use_cases/test_deployment_history_use_case.py +++ b/agentex/tests/unit/use_cases/test_deployment_history_use_case.py @@ -38,7 +38,7 @@ def sample_agent(): id=str(uuid4()), name="test-agent", description="A test agent for deployment history testing", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, status=AgentStatus.READY, acp_url="http://test-acp.example.com", ) @@ -215,7 +215,7 @@ async def test_multiple_agents_same_commit( id=str(uuid4()), name="test-agent-1", description="First test agent", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, status=AgentStatus.READY, acp_url="http://test-acp1.example.com", ) @@ -223,7 +223,7 @@ async def test_multiple_agents_same_commit( id=str(uuid4()), name="test-agent-2", description="Second test agent", - acp_type=ACPType.AGENTIC, + acp_type=ACPType.ASYNC, status=AgentStatus.READY, acp_url="http://test-acp2.example.com", )