asap-protocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asap/__init__.py +7 -0
- asap/cli.py +220 -0
- asap/errors.py +150 -0
- asap/examples/README.md +25 -0
- asap/examples/__init__.py +1 -0
- asap/examples/coordinator.py +184 -0
- asap/examples/echo_agent.py +100 -0
- asap/examples/run_demo.py +120 -0
- asap/models/__init__.py +146 -0
- asap/models/base.py +55 -0
- asap/models/constants.py +14 -0
- asap/models/entities.py +410 -0
- asap/models/enums.py +71 -0
- asap/models/envelope.py +94 -0
- asap/models/ids.py +55 -0
- asap/models/parts.py +207 -0
- asap/models/payloads.py +423 -0
- asap/models/types.py +39 -0
- asap/observability/__init__.py +43 -0
- asap/observability/logging.py +216 -0
- asap/observability/metrics.py +399 -0
- asap/schemas.py +203 -0
- asap/state/__init__.py +22 -0
- asap/state/machine.py +86 -0
- asap/state/snapshot.py +265 -0
- asap/transport/__init__.py +84 -0
- asap/transport/client.py +399 -0
- asap/transport/handlers.py +444 -0
- asap/transport/jsonrpc.py +190 -0
- asap/transport/middleware.py +359 -0
- asap/transport/server.py +739 -0
- asap_protocol-0.1.0.dist-info/METADATA +251 -0
- asap_protocol-0.1.0.dist-info/RECORD +36 -0
- asap_protocol-0.1.0.dist-info/WHEEL +4 -0
- asap_protocol-0.1.0.dist-info/entry_points.txt +2 -0
- asap_protocol-0.1.0.dist-info/licenses/LICENSE +190 -0
asap/models/parts.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Part models for ASAP protocol content types.
|
|
2
|
+
|
|
3
|
+
Parts are atomic content units used within messages and artifacts.
|
|
4
|
+
They support different content types including text, structured data,
|
|
5
|
+
files, MCP resources, and parameterized templates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import re
|
|
10
|
+
from typing import Annotated, Any, Literal, Union
|
|
11
|
+
|
|
12
|
+
from pydantic import Discriminator, Field, TypeAdapter, field_validator
|
|
13
|
+
|
|
14
|
+
from asap.models.base import ASAPBaseModel
|
|
15
|
+
from asap.models.types import MIMEType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextPart(ASAPBaseModel):
|
|
19
|
+
"""Plain text content part.
|
|
20
|
+
|
|
21
|
+
TextPart represents simple text content, such as user messages,
|
|
22
|
+
agent responses, or any textual information.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
type: Discriminator field, always "text"
|
|
26
|
+
content: The text content
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> part = TextPart(
|
|
30
|
+
... type="text",
|
|
31
|
+
... content="Research Q3 market trends for AI infrastructure"
|
|
32
|
+
... )
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
type: Literal["text"] = Field(..., description="Part type discriminator")
|
|
36
|
+
content: str = Field(..., description="Text content")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DataPart(ASAPBaseModel):
|
|
40
|
+
"""Structured JSON data part.
|
|
41
|
+
|
|
42
|
+
DataPart represents structured data in JSON format, optionally
|
|
43
|
+
with a schema URI for validation.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
type: Discriminator field, always "data"
|
|
47
|
+
data: Arbitrary JSON-serializable data
|
|
48
|
+
schema_uri: Optional URI to JSON Schema for validation
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> part = DataPart(
|
|
52
|
+
... type="data",
|
|
53
|
+
... data={"query": "AI trends", "max_results": 10},
|
|
54
|
+
... schema_uri="https://example.com/schemas/search.json"
|
|
55
|
+
... )
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
type: Literal["data"] = Field(..., description="Part type discriminator")
|
|
59
|
+
data: dict[str, Any] = Field(..., description="Structured data (JSON-serializable)")
|
|
60
|
+
schema_uri: str | None = Field(
|
|
61
|
+
default=None, description="Optional JSON Schema URI for validation"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class FilePart(ASAPBaseModel):
|
|
66
|
+
"""Binary or text file part.
|
|
67
|
+
|
|
68
|
+
FilePart represents file content, either by reference (URI) or
|
|
69
|
+
inline (base64-encoded data). Includes MIME type for proper handling.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
type: Discriminator field, always "file"
|
|
73
|
+
uri: File URI (can be asap://, file://, https://, or data: URI)
|
|
74
|
+
mime_type: MIME type of the file (e.g., "application/pdf")
|
|
75
|
+
inline_data: Optional base64-encoded inline file data
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> part = FilePart(
|
|
79
|
+
... type="file",
|
|
80
|
+
... uri="asap://artifacts/task_123/report.pdf",
|
|
81
|
+
... mime_type="application/pdf"
|
|
82
|
+
... )
|
|
83
|
+
>>>
|
|
84
|
+
>>> # With inline data
|
|
85
|
+
>>> part_inline = FilePart(
|
|
86
|
+
... type="file",
|
|
87
|
+
... uri="data:text/plain;base64,SGVsbG8gV29ybGQ=",
|
|
88
|
+
... mime_type="text/plain",
|
|
89
|
+
... inline_data="SGVsbG8gV29ybGQ="
|
|
90
|
+
... )
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
type: Literal["file"] = Field(..., description="Part type discriminator")
|
|
94
|
+
uri: str = Field(..., description="File URI (asap://, file://, https://, data:)")
|
|
95
|
+
mime_type: MIMEType = Field(..., description="MIME type (e.g., application/pdf)")
|
|
96
|
+
inline_data: str | None = Field(
|
|
97
|
+
default=None, description="Optional base64-encoded inline file data"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@field_validator("mime_type")
|
|
101
|
+
@classmethod
|
|
102
|
+
def validate_mime_type(cls, v: str) -> str:
|
|
103
|
+
"""Validate MIME type format (type/subtype)."""
|
|
104
|
+
# Pattern: type/subtype where both parts can contain alphanumeric, dots, plus, and hyphens
|
|
105
|
+
if not re.match(r"^[a-z0-9-]+/[a-z0-9.+\-]+$", v.lower()):
|
|
106
|
+
raise ValueError(f"Invalid MIME type format: {v}")
|
|
107
|
+
return v
|
|
108
|
+
|
|
109
|
+
@field_validator("inline_data")
|
|
110
|
+
@classmethod
|
|
111
|
+
def validate_base64(cls, v: str | None) -> str | None:
|
|
112
|
+
"""Validate that inline_data is valid base64 when provided.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
v: Base64 string or None
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Base64 string after validation
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
ValueError: If inline_data is not valid base64
|
|
122
|
+
"""
|
|
123
|
+
if v is not None:
|
|
124
|
+
try:
|
|
125
|
+
base64.b64decode(v, validate=True)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise ValueError(f"inline_data must be valid base64: {e}") from e
|
|
128
|
+
return v
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ResourcePart(ASAPBaseModel):
|
|
132
|
+
"""Reference to an MCP resource.
|
|
133
|
+
|
|
134
|
+
ResourcePart represents a reference to a resource provided by
|
|
135
|
+
an MCP (Model Context Protocol) server, enabling integration
|
|
136
|
+
with external tools and data sources.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
type: Discriminator field, always "resource"
|
|
140
|
+
resource_uri: URI of the MCP resource
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> part = ResourcePart(
|
|
144
|
+
... type="resource",
|
|
145
|
+
... resource_uri="mcp://tools.example.com/web/search_results"
|
|
146
|
+
... )
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
type: Literal["resource"] = Field(..., description="Part type discriminator")
|
|
150
|
+
resource_uri: str = Field(..., description="MCP resource URI")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TemplatePart(ASAPBaseModel):
|
|
154
|
+
"""Parameterized prompt template.
|
|
155
|
+
|
|
156
|
+
TemplatePart represents a template string with variable placeholders
|
|
157
|
+
(using {{variable}} syntax) and their corresponding values.
|
|
158
|
+
|
|
159
|
+
Attributes:
|
|
160
|
+
type: Discriminator field, always "template"
|
|
161
|
+
template: Template string with {{variable}} placeholders
|
|
162
|
+
variables: Dictionary mapping variable names to their values
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
>>> part = TemplatePart(
|
|
166
|
+
... type="template",
|
|
167
|
+
... template="Research {{topic}} for {{timeframe}}",
|
|
168
|
+
... variables={"topic": "AI trends", "timeframe": "Q3 2025"}
|
|
169
|
+
... )
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
type: Literal["template"] = Field(..., description="Part type discriminator")
|
|
173
|
+
template: str = Field(..., description="Template string with {{variable}} syntax")
|
|
174
|
+
variables: dict[str, Any] = Field(..., description="Variable name to value mapping")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Discriminated union type for type hints
|
|
178
|
+
PartType = Annotated[
|
|
179
|
+
Union[TextPart, DataPart, FilePart, ResourcePart, TemplatePart], Discriminator("type")
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
# TypeAdapter for validation and deserialization
|
|
183
|
+
Part: TypeAdapter[PartType] = TypeAdapter(PartType)
|
|
184
|
+
"""TypeAdapter for Part discriminated union.
|
|
185
|
+
|
|
186
|
+
Part is a TypeAdapter that can validate and deserialize any of the five
|
|
187
|
+
part types: TextPart, DataPart, FilePart, ResourcePart, or TemplatePart.
|
|
188
|
+
|
|
189
|
+
The 'type' field is used as a discriminator to automatically deserialize
|
|
190
|
+
JSON data into the correct Part subtype.
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
>>> from asap.models.parts import Part
|
|
194
|
+
>>>
|
|
195
|
+
>>> # Deserializes to TextPart
|
|
196
|
+
>>> text_part = Part.validate_python({"type": "text", "content": "Hello"})
|
|
197
|
+
>>>
|
|
198
|
+
>>> # Deserializes to DataPart
|
|
199
|
+
>>> data_part = Part.validate_python({"type": "data", "data": {"key": "value"}})
|
|
200
|
+
>>>
|
|
201
|
+
>>> # Deserializes to FilePart
|
|
202
|
+
>>> file_part = Part.validate_python({
|
|
203
|
+
... "type": "file",
|
|
204
|
+
... "uri": "file://test.pdf",
|
|
205
|
+
... "mime_type": "application/pdf"
|
|
206
|
+
... })
|
|
207
|
+
"""
|
asap/models/payloads.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Payload models for ASAP protocol messages.
|
|
2
|
+
|
|
3
|
+
Payloads define the content of protocol messages for different operations
|
|
4
|
+
like task requests, responses, updates, state management, and MCP integration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Union
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, field_validator, model_validator
|
|
10
|
+
|
|
11
|
+
from asap.models.base import ASAPBaseModel
|
|
12
|
+
from asap.models.enums import MessageRole, TaskStatus, UpdateType
|
|
13
|
+
from asap.models.types import (
|
|
14
|
+
AgentURN,
|
|
15
|
+
ArtifactID,
|
|
16
|
+
ConversationID,
|
|
17
|
+
MessageID,
|
|
18
|
+
PartID,
|
|
19
|
+
SnapshotID,
|
|
20
|
+
TaskID,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaskRequest(ASAPBaseModel):
|
|
25
|
+
"""Request to execute a task on an agent.
|
|
26
|
+
|
|
27
|
+
TaskRequest initiates task execution, specifying the skill to invoke,
|
|
28
|
+
input data, and optional configuration parameters.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
conversation_id: ID of the conversation this task belongs to
|
|
32
|
+
parent_task_id: Optional ID of parent task (for subtasks)
|
|
33
|
+
skill_id: Identifier of the skill to execute
|
|
34
|
+
input: Input data for the skill (JSON-serializable)
|
|
35
|
+
config: Optional configuration (timeout, priority, streaming, etc.)
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> request = TaskRequest(
|
|
39
|
+
... conversation_id="conv_01HX5K3MQVN8",
|
|
40
|
+
... skill_id="web_research",
|
|
41
|
+
... input={"query": "AI infrastructure market trends Q3 2025"},
|
|
42
|
+
... config={"timeout_seconds": 600, "streaming": True}
|
|
43
|
+
... )
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
conversation_id: ConversationID = Field(..., description="Parent conversation ID")
|
|
47
|
+
parent_task_id: TaskID | None = Field(default=None, description="Parent task ID for subtasks")
|
|
48
|
+
skill_id: str = Field(..., description="Skill identifier to execute")
|
|
49
|
+
input: dict[str, Any] = Field(..., description="Input data for the skill")
|
|
50
|
+
config: dict[str, Any] | None = Field(
|
|
51
|
+
default=None, description="Optional configuration (timeout, priority, streaming, etc.)"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TaskResponse(ASAPBaseModel):
|
|
56
|
+
"""Response to a task execution request.
|
|
57
|
+
|
|
58
|
+
TaskResponse provides the final result of task execution, including
|
|
59
|
+
status, result data, final state snapshot, and execution metrics.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
task_id: ID of the completed task
|
|
63
|
+
status: Final task status (completed, failed, cancelled, etc.)
|
|
64
|
+
result: Optional result data (summary, artifacts, etc.)
|
|
65
|
+
final_state: Optional final state snapshot
|
|
66
|
+
metrics: Optional execution metrics (duration, tokens used, etc.)
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> response = TaskResponse(
|
|
70
|
+
... task_id="task_01HX5K4N",
|
|
71
|
+
... status="completed",
|
|
72
|
+
... result={"summary": "Analysis complete", "artifacts": ["art_123"]},
|
|
73
|
+
... metrics={"duration_ms": 45000, "tokens_used": 12500}
|
|
74
|
+
... )
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
task_id: TaskID = Field(..., description="Task identifier")
|
|
78
|
+
status: TaskStatus = Field(..., description="Final task status")
|
|
79
|
+
result: dict[str, Any] | None = Field(
|
|
80
|
+
default=None, description="Result data (summary, artifacts, etc.)"
|
|
81
|
+
)
|
|
82
|
+
final_state: dict[str, Any] | None = Field(default=None, description="Final state snapshot")
|
|
83
|
+
metrics: dict[str, Any] | None = Field(
|
|
84
|
+
default=None, description="Execution metrics (duration, tokens, etc.)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TaskUpdate(ASAPBaseModel):
|
|
89
|
+
"""Update on task execution progress or status.
|
|
90
|
+
|
|
91
|
+
TaskUpdate provides real-time updates during task execution, including
|
|
92
|
+
progress information or requests for additional input.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
task_id: ID of the task being updated
|
|
96
|
+
update_type: Type of update (progress, input_required, etc.)
|
|
97
|
+
status: Current task status
|
|
98
|
+
progress: Optional progress information (percent, message, ETA)
|
|
99
|
+
input_request: Optional request for additional input from user
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> # Progress update
|
|
103
|
+
>>> update = TaskUpdate(
|
|
104
|
+
... task_id="task_123",
|
|
105
|
+
... update_type="progress",
|
|
106
|
+
... status="working",
|
|
107
|
+
... progress={"percent": 65, "message": "Synthesizing findings..."}
|
|
108
|
+
... )
|
|
109
|
+
>>>
|
|
110
|
+
>>> # Input required update
|
|
111
|
+
>>> update = TaskUpdate(
|
|
112
|
+
... task_id="task_123",
|
|
113
|
+
... update_type="input_required",
|
|
114
|
+
... status="input_required",
|
|
115
|
+
... input_request={"prompt": "Please clarify:", "options": [...]}
|
|
116
|
+
... )
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
task_id: TaskID = Field(..., description="Task identifier")
|
|
120
|
+
update_type: UpdateType = Field(..., description="Update type (progress, input_required)")
|
|
121
|
+
status: TaskStatus = Field(..., description="Current task status")
|
|
122
|
+
progress: dict[str, Any] | None = Field(
|
|
123
|
+
default=None, description="Progress info (percent, message, ETA)"
|
|
124
|
+
)
|
|
125
|
+
input_request: dict[str, Any] | None = Field(
|
|
126
|
+
default=None, description="Request for additional input"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@field_validator("progress")
|
|
130
|
+
@classmethod
|
|
131
|
+
def validate_progress_percent(cls, v: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
132
|
+
"""Validate that progress percent is between 0 and 100 if provided.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
v: Progress dictionary or None
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Progress dictionary after validation
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If percent is not between 0 and 100
|
|
142
|
+
"""
|
|
143
|
+
if v and "percent" in v:
|
|
144
|
+
percent = v["percent"]
|
|
145
|
+
if not isinstance(percent, (int, float)):
|
|
146
|
+
raise ValueError("progress.percent must be a number")
|
|
147
|
+
if not (0 <= percent <= 100):
|
|
148
|
+
raise ValueError("progress.percent must be between 0 and 100")
|
|
149
|
+
return v
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TaskCancel(ASAPBaseModel):
|
|
153
|
+
"""Request to cancel a running task.
|
|
154
|
+
|
|
155
|
+
TaskCancel requests cancellation of a task that is currently executing.
|
|
156
|
+
The agent should attempt graceful cancellation and cleanup.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
task_id: ID of the task to cancel
|
|
160
|
+
reason: Optional reason for cancellation
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> cancel = TaskCancel(
|
|
164
|
+
... task_id="task_123",
|
|
165
|
+
... reason="User requested cancellation"
|
|
166
|
+
... )
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
task_id: TaskID = Field(..., description="Task identifier to cancel")
|
|
170
|
+
reason: str | None = Field(default=None, description="Optional cancellation reason")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class MessageSend(ASAPBaseModel):
|
|
174
|
+
"""Send a message within a task conversation.
|
|
175
|
+
|
|
176
|
+
MessageSend exchanges conversation turns between agents during
|
|
177
|
+
task execution, containing message content as parts.
|
|
178
|
+
|
|
179
|
+
Attributes:
|
|
180
|
+
task_id: ID of the task this message belongs to
|
|
181
|
+
message_id: Unique identifier for this message
|
|
182
|
+
sender: Agent URN of the message sender
|
|
183
|
+
role: Message role (user, assistant, system)
|
|
184
|
+
parts: List of part IDs that make up this message
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> message = MessageSend(
|
|
188
|
+
... task_id="task_123",
|
|
189
|
+
... message_id="msg_456",
|
|
190
|
+
... sender="urn:asap:agent:coordinator",
|
|
191
|
+
... role="user",
|
|
192
|
+
... parts=["part_789"]
|
|
193
|
+
... )
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
task_id: TaskID = Field(..., description="Parent task ID")
|
|
197
|
+
message_id: MessageID = Field(..., description="Unique message identifier")
|
|
198
|
+
sender: AgentURN = Field(..., description="Sender agent URN")
|
|
199
|
+
role: MessageRole = Field(..., description="Message role (user, assistant, system)")
|
|
200
|
+
parts: list[PartID] = Field(..., description="Part IDs making up this message")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class StateQuery(ASAPBaseModel):
|
|
204
|
+
"""Request a state snapshot for a task.
|
|
205
|
+
|
|
206
|
+
StateQuery requests the current or a specific version of a task's
|
|
207
|
+
state snapshot for inspection or restoration.
|
|
208
|
+
|
|
209
|
+
Attributes:
|
|
210
|
+
task_id: ID of the task to query state for
|
|
211
|
+
version: Optional specific version number to retrieve
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
>>> # Query latest state
|
|
215
|
+
>>> query = StateQuery(task_id="task_123")
|
|
216
|
+
>>>
|
|
217
|
+
>>> # Query specific version
|
|
218
|
+
>>> query = StateQuery(task_id="task_123", version=5)
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
task_id: TaskID = Field(..., description="Task identifier")
|
|
222
|
+
version: int | None = Field(default=None, description="Optional specific version to retrieve")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class StateRestore(ASAPBaseModel):
|
|
226
|
+
"""Restore a task to a previous state snapshot.
|
|
227
|
+
|
|
228
|
+
StateRestore requests restoration of a task to a previously saved
|
|
229
|
+
state snapshot, enabling rollback and recovery scenarios.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
task_id: ID of the task to restore
|
|
233
|
+
snapshot_id: ID of the snapshot to restore from
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
>>> restore = StateRestore(
|
|
237
|
+
... task_id="task_123",
|
|
238
|
+
... snapshot_id="snap_456"
|
|
239
|
+
... )
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
task_id: TaskID = Field(..., description="Task identifier")
|
|
243
|
+
snapshot_id: SnapshotID = Field(..., description="Snapshot ID to restore from")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class ArtifactNotify(ASAPBaseModel):
|
|
247
|
+
"""Notify about artifact creation or availability.
|
|
248
|
+
|
|
249
|
+
ArtifactNotify informs agents when a new artifact has been created
|
|
250
|
+
or is available for retrieval.
|
|
251
|
+
|
|
252
|
+
Attributes:
|
|
253
|
+
artifact_id: ID of the artifact
|
|
254
|
+
task_id: ID of the task that produced the artifact
|
|
255
|
+
name: Optional human-readable artifact name
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
>>> notify = ArtifactNotify(
|
|
259
|
+
... artifact_id="art_123",
|
|
260
|
+
... task_id="task_456",
|
|
261
|
+
... name="Q3 Market Analysis Report"
|
|
262
|
+
... )
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
artifact_id: ArtifactID = Field(..., description="Artifact identifier")
|
|
266
|
+
task_id: TaskID = Field(..., description="Parent task ID")
|
|
267
|
+
name: str | None = Field(default=None, description="Optional human-readable artifact name")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class McpToolCall(ASAPBaseModel):
|
|
271
|
+
"""Call an MCP tool.
|
|
272
|
+
|
|
273
|
+
McpToolCall invokes a tool provided by an MCP server, enabling
|
|
274
|
+
agents to leverage external capabilities and integrations.
|
|
275
|
+
|
|
276
|
+
Attributes:
|
|
277
|
+
request_id: Unique identifier for this tool call request
|
|
278
|
+
tool_name: Name of the MCP tool to invoke
|
|
279
|
+
arguments: Arguments to pass to the tool (JSON-serializable)
|
|
280
|
+
mcp_context: Optional MCP-specific context (server, session, etc.)
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> tool_call = McpToolCall(
|
|
284
|
+
... request_id="req_123",
|
|
285
|
+
... tool_name="web_search",
|
|
286
|
+
... arguments={"query": "AI trends", "max_results": 10},
|
|
287
|
+
... mcp_context={"server": "mcp://tools.example.com"}
|
|
288
|
+
... )
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
request_id: str = Field(..., description="Unique request identifier")
|
|
292
|
+
tool_name: str = Field(..., description="MCP tool name to invoke")
|
|
293
|
+
arguments: dict[str, Any] = Field(..., description="Tool arguments (JSON-serializable)")
|
|
294
|
+
mcp_context: dict[str, Any] | None = Field(
|
|
295
|
+
default=None, description="Optional MCP context (server, session, etc.)"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class McpToolResult(ASAPBaseModel):
|
|
300
|
+
"""Result of an MCP tool call.
|
|
301
|
+
|
|
302
|
+
McpToolResult provides the outcome of an MCP tool invocation,
|
|
303
|
+
including success status, result data, or error information.
|
|
304
|
+
|
|
305
|
+
Attributes:
|
|
306
|
+
request_id: ID of the original tool call request
|
|
307
|
+
success: Whether the tool call succeeded
|
|
308
|
+
result: Optional result data (if successful)
|
|
309
|
+
error: Optional error message (if failed)
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
>>> # Successful result
|
|
313
|
+
>>> result = McpToolResult(
|
|
314
|
+
... request_id="req_123",
|
|
315
|
+
... success=True,
|
|
316
|
+
... result={"findings": ["finding1", "finding2"]}
|
|
317
|
+
... )
|
|
318
|
+
>>>
|
|
319
|
+
>>> # Failed result
|
|
320
|
+
>>> result = McpToolResult(
|
|
321
|
+
... request_id="req_123",
|
|
322
|
+
... success=False,
|
|
323
|
+
... error="Tool execution failed: timeout"
|
|
324
|
+
... )
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
request_id: str = Field(..., description="Original request identifier")
|
|
328
|
+
success: bool = Field(..., description="Whether tool call succeeded")
|
|
329
|
+
result: dict[str, Any] | None = Field(default=None, description="Result data (if successful)")
|
|
330
|
+
error: str | None = Field(default=None, description="Error message (if failed)")
|
|
331
|
+
|
|
332
|
+
@model_validator(mode="after")
|
|
333
|
+
def validate_result_error_exclusivity(self) -> "McpToolResult":
|
|
334
|
+
"""Validate that result and error are mutually exclusive based on success.
|
|
335
|
+
|
|
336
|
+
When success=True, result must be provided and error must be None.
|
|
337
|
+
When success=False, error must be provided and result must be None.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Self after validation
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
ValueError: If result/error are not mutually exclusive based on success
|
|
344
|
+
"""
|
|
345
|
+
if self.success:
|
|
346
|
+
if self.result is None:
|
|
347
|
+
raise ValueError("result must be provided when success=True")
|
|
348
|
+
if self.error is not None:
|
|
349
|
+
raise ValueError("error must be None when success=True")
|
|
350
|
+
else:
|
|
351
|
+
if self.error is None:
|
|
352
|
+
raise ValueError("error must be provided when success=False")
|
|
353
|
+
if self.result is not None:
|
|
354
|
+
raise ValueError("result must be None when success=False")
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class McpResourceFetch(ASAPBaseModel):
|
|
359
|
+
"""Request to fetch an MCP resource.
|
|
360
|
+
|
|
361
|
+
McpResourceFetch requests retrieval of a resource from an MCP server,
|
|
362
|
+
such as documentation, data, or other content.
|
|
363
|
+
|
|
364
|
+
Attributes:
|
|
365
|
+
resource_uri: URI of the MCP resource to fetch
|
|
366
|
+
|
|
367
|
+
Example:
|
|
368
|
+
>>> fetch = McpResourceFetch(
|
|
369
|
+
... resource_uri="mcp://server/resources/data_123"
|
|
370
|
+
... )
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
resource_uri: str = Field(..., description="MCP resource URI to fetch")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class McpResourceData(ASAPBaseModel):
|
|
377
|
+
"""Data from an MCP resource.
|
|
378
|
+
|
|
379
|
+
McpResourceData provides the content of a fetched MCP resource.
|
|
380
|
+
|
|
381
|
+
Attributes:
|
|
382
|
+
resource_uri: URI of the resource
|
|
383
|
+
content: Resource content (JSON-serializable)
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
>>> data = McpResourceData(
|
|
387
|
+
... resource_uri="mcp://server/resources/data_123",
|
|
388
|
+
... content={"data": [1, 2, 3], "metadata": {"source": "api"}}
|
|
389
|
+
... )
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
resource_uri: str = Field(..., description="MCP resource URI")
|
|
393
|
+
content: dict[str, Any] = Field(..., description="Resource content (JSON-serializable)")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Union type for all payload types
|
|
397
|
+
# Note: The discriminator (payload_type) will be in the Envelope, not in individual payloads
|
|
398
|
+
PayloadType = Union[
|
|
399
|
+
TaskRequest,
|
|
400
|
+
TaskResponse,
|
|
401
|
+
TaskUpdate,
|
|
402
|
+
TaskCancel,
|
|
403
|
+
MessageSend,
|
|
404
|
+
StateQuery,
|
|
405
|
+
StateRestore,
|
|
406
|
+
ArtifactNotify,
|
|
407
|
+
McpToolCall,
|
|
408
|
+
McpToolResult,
|
|
409
|
+
McpResourceFetch,
|
|
410
|
+
McpResourceData,
|
|
411
|
+
]
|
|
412
|
+
"""Union type of all ASAP payload types.
|
|
413
|
+
|
|
414
|
+
PayloadType represents any of the 12 payload types used in ASAP protocol messages.
|
|
415
|
+
The actual payload type discrimination is done via the 'payload_type' field in the
|
|
416
|
+
Envelope that wraps these payloads.
|
|
417
|
+
|
|
418
|
+
Payload types:
|
|
419
|
+
- Task operations: TaskRequest, TaskResponse, TaskUpdate, TaskCancel
|
|
420
|
+
- Messaging: MessageSend
|
|
421
|
+
- State management: StateQuery, StateRestore, ArtifactNotify
|
|
422
|
+
- MCP integration: McpToolCall, McpToolResult, McpResourceFetch, McpResourceData
|
|
423
|
+
"""
|
asap/models/types.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Type aliases for ASAP protocol.
|
|
2
|
+
|
|
3
|
+
This module defines type aliases to improve code readability and
|
|
4
|
+
document the semantic meaning of string types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
# Entity identifiers
|
|
10
|
+
AgentURN: TypeAlias = str
|
|
11
|
+
"""Agent identifier in URN format: urn:asap:agent:{name}"""
|
|
12
|
+
|
|
13
|
+
ConversationID: TypeAlias = str
|
|
14
|
+
"""Unique conversation identifier (ULID format)"""
|
|
15
|
+
|
|
16
|
+
TaskID: TypeAlias = str
|
|
17
|
+
"""Unique task identifier (ULID format)"""
|
|
18
|
+
|
|
19
|
+
MessageID: TypeAlias = str
|
|
20
|
+
"""Unique message identifier (ULID format)"""
|
|
21
|
+
|
|
22
|
+
ArtifactID: TypeAlias = str
|
|
23
|
+
"""Unique artifact identifier (ULID format)"""
|
|
24
|
+
|
|
25
|
+
SnapshotID: TypeAlias = str
|
|
26
|
+
"""Unique state snapshot identifier (ULID format)"""
|
|
27
|
+
|
|
28
|
+
PartID: TypeAlias = str
|
|
29
|
+
"""Unique part identifier (ULID format)"""
|
|
30
|
+
|
|
31
|
+
# Other semantic types
|
|
32
|
+
URI: TypeAlias = str
|
|
33
|
+
"""Uniform Resource Identifier"""
|
|
34
|
+
|
|
35
|
+
MIMEType: TypeAlias = str
|
|
36
|
+
"""MIME type string (e.g., 'application/json')"""
|
|
37
|
+
|
|
38
|
+
SemanticVersion: TypeAlias = str
|
|
39
|
+
"""Semantic version string (e.g., '1.0.0')"""
|