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/entities.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""Core entity models for the ASAP protocol.
|
|
2
|
+
|
|
3
|
+
This module defines the fundamental entities used in agent-to-agent communication:
|
|
4
|
+
- Agent: An autonomous entity capable of sending/receiving ASAP messages
|
|
5
|
+
- Manifest: Self-describing metadata about an agent's capabilities
|
|
6
|
+
- Conversation: Context for related interactions between agents
|
|
7
|
+
- Task: Fundamental unit of work with lifecycle management
|
|
8
|
+
- Message: Single communication turn containing parts
|
|
9
|
+
- Artifact: Concrete output produced by task execution
|
|
10
|
+
- StateSnapshot: First-class state persistence mechanism
|
|
11
|
+
- Skill: A specific capability that an agent can perform
|
|
12
|
+
- Capability: Collection of an agent's features and supported operations
|
|
13
|
+
- Endpoint: Network endpoints for agent communication
|
|
14
|
+
- AuthScheme: Authentication configuration for agent access
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from packaging.version import InvalidVersion, Version
|
|
22
|
+
from pydantic import Field, field_validator
|
|
23
|
+
|
|
24
|
+
from asap.models.base import ASAPBaseModel
|
|
25
|
+
from asap.models.constants import AGENT_URN_PATTERN, ASAP_PROTOCOL_VERSION
|
|
26
|
+
from asap.models.enums import MessageRole, TaskStatus
|
|
27
|
+
from asap.models.types import (
|
|
28
|
+
AgentURN,
|
|
29
|
+
ArtifactID,
|
|
30
|
+
ConversationID,
|
|
31
|
+
MessageID,
|
|
32
|
+
PartID,
|
|
33
|
+
SemanticVersion,
|
|
34
|
+
SnapshotID,
|
|
35
|
+
TaskID,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Skill(ASAPBaseModel):
|
|
40
|
+
"""A specific capability that an agent can perform.
|
|
41
|
+
|
|
42
|
+
Skills define what an agent can do, along with the expected input
|
|
43
|
+
and output schemas for validation.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
id: Unique identifier for the skill (e.g., "web_research")
|
|
47
|
+
description: Human-readable description of what the skill does
|
|
48
|
+
input_schema: Optional JSON Schema for validating skill inputs
|
|
49
|
+
output_schema: Optional JSON Schema for validating skill outputs
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> skill = Skill(
|
|
53
|
+
... id="web_research",
|
|
54
|
+
... description="Search and synthesize information from the web",
|
|
55
|
+
... input_schema={"type": "object", "properties": {"query": {"type": "string"}}},
|
|
56
|
+
... output_schema={"type": "object", "properties": {"summary": {"type": "string"}}}
|
|
57
|
+
... )
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
id: str = Field(..., description="Unique skill identifier")
|
|
61
|
+
description: str = Field(..., description="Human-readable skill description")
|
|
62
|
+
input_schema: dict[str, Any] | None = Field(
|
|
63
|
+
default=None, description="JSON Schema for skill input validation"
|
|
64
|
+
)
|
|
65
|
+
output_schema: dict[str, Any] | None = Field(
|
|
66
|
+
default=None, description="JSON Schema for skill output validation"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Capability(ASAPBaseModel):
|
|
71
|
+
"""Collection of an agent's features and supported operations.
|
|
72
|
+
|
|
73
|
+
Capabilities describe what an agent can do, including skills,
|
|
74
|
+
state persistence, streaming support, and MCP tool integration.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
asap_version: ASAP protocol version supported (e.g., "0.1")
|
|
78
|
+
skills: List of skills the agent can perform
|
|
79
|
+
state_persistence: Whether the agent supports state snapshots
|
|
80
|
+
streaming: Whether the agent supports streaming responses
|
|
81
|
+
mcp_tools: List of MCP tool names the agent can execute
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> capability = Capability(
|
|
85
|
+
... asap_version="0.1",
|
|
86
|
+
... skills=[Skill(id="research", description="Research skill")],
|
|
87
|
+
... state_persistence=True,
|
|
88
|
+
... streaming=True,
|
|
89
|
+
... mcp_tools=["web_search"]
|
|
90
|
+
... )
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
asap_version: str = Field(default=ASAP_PROTOCOL_VERSION, description="ASAP protocol version")
|
|
94
|
+
skills: list[Skill] = Field(default_factory=list, description="Available skills")
|
|
95
|
+
state_persistence: bool = Field(default=False, description="Supports state snapshots")
|
|
96
|
+
streaming: bool = Field(default=False, description="Supports streaming responses")
|
|
97
|
+
mcp_tools: list[str] = Field(default_factory=list, description="Available MCP tools")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Endpoint(ASAPBaseModel):
|
|
101
|
+
"""Network endpoints for agent communication.
|
|
102
|
+
|
|
103
|
+
Defines the URLs where an agent can be reached for different
|
|
104
|
+
types of communication.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
asap: HTTP endpoint for ASAP protocol messages (required)
|
|
108
|
+
events: Optional WebSocket endpoint for streaming events
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> endpoint = Endpoint(
|
|
112
|
+
... asap="https://api.example.com/asap",
|
|
113
|
+
... events="wss://api.example.com/asap/events"
|
|
114
|
+
... )
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
asap: str = Field(..., description="HTTP endpoint for ASAP messages")
|
|
118
|
+
events: str | None = Field(default=None, description="WebSocket endpoint for streaming events")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class AuthScheme(ASAPBaseModel):
|
|
122
|
+
"""Authentication configuration for agent access.
|
|
123
|
+
|
|
124
|
+
Defines the authentication methods supported by an agent and
|
|
125
|
+
optional OAuth2 configuration.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
schemes: List of supported auth schemes (e.g., ["bearer", "oauth2"])
|
|
129
|
+
oauth2: Optional OAuth2 configuration with URLs and scopes
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
>>> auth = AuthScheme(
|
|
133
|
+
... schemes=["bearer", "oauth2"],
|
|
134
|
+
... oauth2={
|
|
135
|
+
... "authorization_url": "https://auth.example.com/authorize",
|
|
136
|
+
... "token_url": "https://auth.example.com/token",
|
|
137
|
+
... "scopes": ["asap:execute", "asap:read"]
|
|
138
|
+
... }
|
|
139
|
+
... )
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
schemes: list[str] = Field(..., description="Supported authentication schemes")
|
|
143
|
+
oauth2: dict[str, Any] | None = Field(
|
|
144
|
+
default=None, description="OAuth2 configuration if oauth2 is in schemes"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Agent(ASAPBaseModel):
|
|
149
|
+
"""An autonomous entity capable of sending and receiving ASAP messages.
|
|
150
|
+
|
|
151
|
+
Agents are symmetric peers with no inherent client/server distinction.
|
|
152
|
+
Each agent has a unique identifier and publishes a manifest describing
|
|
153
|
+
its capabilities.
|
|
154
|
+
|
|
155
|
+
Attributes:
|
|
156
|
+
id: Unique agent identifier (URN format, e.g., "urn:asap:agent:research-v1")
|
|
157
|
+
manifest_uri: URL where the agent's manifest can be retrieved
|
|
158
|
+
capabilities: List of capability strings (e.g., ["task.execute", "mcp.tools"])
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
>>> agent = Agent(
|
|
162
|
+
... id="urn:asap:agent:research-v1",
|
|
163
|
+
... manifest_uri="https://agents.example.com/.well-known/asap/research-v1.json",
|
|
164
|
+
... capabilities=["task.execute", "state.persist", "mcp.tools"]
|
|
165
|
+
... )
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
id: AgentURN = Field(..., description="Unique agent identifier (URN format)")
|
|
169
|
+
manifest_uri: str = Field(..., description="URL to agent's manifest")
|
|
170
|
+
capabilities: list[str] = Field(..., min_length=1, description="Agent capability strings")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Manifest(ASAPBaseModel):
|
|
174
|
+
"""Self-describing metadata about an agent's capabilities.
|
|
175
|
+
|
|
176
|
+
The manifest is analogous to A2A's Agent Card but extended with
|
|
177
|
+
additional ASAP-specific features. It provides all information
|
|
178
|
+
needed to interact with an agent.
|
|
179
|
+
|
|
180
|
+
Attributes:
|
|
181
|
+
id: Unique agent identifier (matches Agent.id)
|
|
182
|
+
name: Human-readable agent name
|
|
183
|
+
version: Semantic version of the agent
|
|
184
|
+
description: Description of what the agent does
|
|
185
|
+
capabilities: Detailed capability information
|
|
186
|
+
endpoints: Network endpoints for communication
|
|
187
|
+
auth: Optional authentication configuration
|
|
188
|
+
signature: Optional cryptographic signature for manifest verification
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> manifest = Manifest(
|
|
192
|
+
... id="urn:asap:agent:research-v1",
|
|
193
|
+
... name="Research Agent",
|
|
194
|
+
... version="1.0.0",
|
|
195
|
+
... description="Performs web research and summarization",
|
|
196
|
+
... capabilities=Capability(
|
|
197
|
+
... asap_version="0.1",
|
|
198
|
+
... skills=[Skill(id="web_research", description="Research skill")],
|
|
199
|
+
... state_persistence=True
|
|
200
|
+
... ),
|
|
201
|
+
... endpoints=Endpoint(asap="https://api.example.com/asap")
|
|
202
|
+
... )
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
id: AgentURN = Field(..., description="Unique agent identifier (URN format)")
|
|
206
|
+
name: str = Field(..., description="Human-readable agent name")
|
|
207
|
+
version: SemanticVersion = Field(..., description="Semantic version (e.g., '1.0.0')")
|
|
208
|
+
description: str = Field(..., description="What the agent does")
|
|
209
|
+
capabilities: Capability = Field(..., description="Agent capabilities")
|
|
210
|
+
endpoints: Endpoint = Field(..., description="Communication endpoints")
|
|
211
|
+
auth: AuthScheme | None = Field(default=None, description="Authentication configuration")
|
|
212
|
+
signature: str | None = Field(
|
|
213
|
+
default=None, description="Cryptographic signature for verification"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@field_validator("id")
|
|
217
|
+
@classmethod
|
|
218
|
+
def validate_urn_format(cls, v: str) -> str:
|
|
219
|
+
"""Validate that agent ID follows URN format."""
|
|
220
|
+
if not re.match(AGENT_URN_PATTERN, v):
|
|
221
|
+
raise ValueError(f"Agent ID must follow URN format 'urn:asap:agent:{{name}}', got: {v}")
|
|
222
|
+
return v
|
|
223
|
+
|
|
224
|
+
@field_validator("version")
|
|
225
|
+
@classmethod
|
|
226
|
+
def validate_semver(cls, v: str) -> str:
|
|
227
|
+
"""Validate semantic versioning format."""
|
|
228
|
+
try:
|
|
229
|
+
Version(v)
|
|
230
|
+
except InvalidVersion as e:
|
|
231
|
+
raise ValueError(f"Invalid semantic version '{v}': {e}") from e
|
|
232
|
+
return v
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Conversation(ASAPBaseModel):
|
|
236
|
+
"""A context for related interactions between agents.
|
|
237
|
+
|
|
238
|
+
Conversations enable shared context accumulation, task grouping,
|
|
239
|
+
and state isolation between unrelated work.
|
|
240
|
+
|
|
241
|
+
Attributes:
|
|
242
|
+
id: Unique conversation identifier (ULID format)
|
|
243
|
+
participants: List of agent URNs participating in the conversation
|
|
244
|
+
created_at: Timestamp when the conversation was created (UTC)
|
|
245
|
+
metadata: Optional metadata (e.g., purpose, TTL, tags)
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
>>> from datetime import datetime, timezone
|
|
249
|
+
>>> conversation = Conversation(
|
|
250
|
+
... id="conv_01HX5K3MQVN8...",
|
|
251
|
+
... participants=["urn:asap:agent:coordinator", "urn:asap:agent:research-v1"],
|
|
252
|
+
... created_at=datetime.now(timezone.utc),
|
|
253
|
+
... metadata={"purpose": "quarterly_report_research", "ttl_hours": 72}
|
|
254
|
+
... )
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
id: ConversationID = Field(..., description="Unique conversation identifier (ULID)")
|
|
258
|
+
participants: list[AgentURN] = Field(
|
|
259
|
+
..., min_length=1, description="Agent URNs in conversation"
|
|
260
|
+
)
|
|
261
|
+
created_at: datetime = Field(..., description="Creation timestamp (UTC)")
|
|
262
|
+
metadata: dict[str, Any] | None = Field(
|
|
263
|
+
default=None, description="Optional metadata (purpose, TTL, etc.)"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Task(ASAPBaseModel):
|
|
268
|
+
"""The fundamental unit of work in ASAP.
|
|
269
|
+
|
|
270
|
+
Tasks are uniquely identified, stateful with a defined lifecycle,
|
|
271
|
+
cancellable, resumable, and capable of producing artifacts.
|
|
272
|
+
|
|
273
|
+
Attributes:
|
|
274
|
+
id: Unique task identifier (ULID format)
|
|
275
|
+
conversation_id: ID of the conversation this task belongs to
|
|
276
|
+
parent_task_id: Optional ID of parent task (for subtasks)
|
|
277
|
+
status: Current task status (submitted, working, completed, etc.)
|
|
278
|
+
progress: Optional progress information (percent, message, ETA)
|
|
279
|
+
created_at: Timestamp when the task was created (UTC)
|
|
280
|
+
updated_at: Timestamp of last status update (UTC)
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> from datetime import datetime, timezone
|
|
284
|
+
>>> task = Task(
|
|
285
|
+
... id="task_01HX5K4N...",
|
|
286
|
+
... conversation_id="conv_01HX5K3MQVN8...",
|
|
287
|
+
... status="working",
|
|
288
|
+
... progress={"percent": 45, "message": "Analyzing search results..."},
|
|
289
|
+
... created_at=datetime.now(timezone.utc),
|
|
290
|
+
... updated_at=datetime.now(timezone.utc)
|
|
291
|
+
... )
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
id: TaskID = Field(..., description="Unique task identifier (ULID)")
|
|
295
|
+
conversation_id: ConversationID = Field(..., description="Parent conversation ID")
|
|
296
|
+
parent_task_id: TaskID | None = Field(default=None, description="Parent task ID for subtasks")
|
|
297
|
+
status: TaskStatus = Field(..., description="Task status (submitted, working, etc.)")
|
|
298
|
+
progress: dict[str, Any] | None = Field(
|
|
299
|
+
default=None, description="Progress info (percent, message, ETA)"
|
|
300
|
+
)
|
|
301
|
+
created_at: datetime = Field(..., description="Creation timestamp (UTC)")
|
|
302
|
+
updated_at: datetime = Field(..., description="Last update timestamp (UTC)")
|
|
303
|
+
|
|
304
|
+
def is_terminal(self) -> bool:
|
|
305
|
+
"""Check if task is in a terminal state (completed, failed, or cancelled)."""
|
|
306
|
+
return self.status.is_terminal()
|
|
307
|
+
|
|
308
|
+
def can_be_cancelled(self) -> bool:
|
|
309
|
+
"""Check if task can be cancelled (only submitted or working tasks)."""
|
|
310
|
+
return self.status in {TaskStatus.SUBMITTED, TaskStatus.WORKING}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class Message(ASAPBaseModel):
|
|
314
|
+
"""A single communication turn containing one or more parts.
|
|
315
|
+
|
|
316
|
+
Messages are the atomic units of communication within tasks,
|
|
317
|
+
containing content parts and metadata about the sender and role.
|
|
318
|
+
|
|
319
|
+
Attributes:
|
|
320
|
+
id: Unique message identifier (ULID format)
|
|
321
|
+
task_id: ID of the task this message belongs to
|
|
322
|
+
sender: Agent URN of the message sender
|
|
323
|
+
role: Message role (user, assistant, system)
|
|
324
|
+
parts: List of part IDs or part references
|
|
325
|
+
timestamp: When the message was sent (UTC)
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
>>> from datetime import datetime, timezone
|
|
329
|
+
>>> message = Message(
|
|
330
|
+
... id="msg_01HX5K5P...",
|
|
331
|
+
... task_id="task_01HX5K4N...",
|
|
332
|
+
... sender="urn:asap:agent:coordinator",
|
|
333
|
+
... role="user",
|
|
334
|
+
... parts=["part_01HX5K...", "part_01HX5L..."],
|
|
335
|
+
... timestamp=datetime.now(timezone.utc)
|
|
336
|
+
... )
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
id: MessageID = Field(..., description="Unique message identifier (ULID)")
|
|
340
|
+
task_id: TaskID = Field(..., description="Parent task ID")
|
|
341
|
+
sender: AgentURN = Field(..., description="Sender agent URN")
|
|
342
|
+
role: MessageRole = Field(..., description="Message role (user, assistant, system)")
|
|
343
|
+
parts: list[PartID] = Field(..., description="Part IDs or references")
|
|
344
|
+
timestamp: datetime = Field(..., description="Message timestamp (UTC)")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class Artifact(ASAPBaseModel):
|
|
348
|
+
"""Concrete output produced by task execution.
|
|
349
|
+
|
|
350
|
+
Artifacts represent the tangible results of task completion,
|
|
351
|
+
such as reports, data files, or other generated content.
|
|
352
|
+
|
|
353
|
+
Attributes:
|
|
354
|
+
id: Unique artifact identifier (ULID format)
|
|
355
|
+
task_id: ID of the task that produced this artifact
|
|
356
|
+
name: Human-readable artifact name
|
|
357
|
+
parts: List of part IDs that make up this artifact
|
|
358
|
+
created_at: When the artifact was created (UTC)
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
>>> from datetime import datetime, timezone
|
|
362
|
+
>>> artifact = Artifact(
|
|
363
|
+
... id="art_01HX5K6Q...",
|
|
364
|
+
... task_id="task_01HX5K4N...",
|
|
365
|
+
... name="Q3 Market Analysis Report",
|
|
366
|
+
... parts=["part_01HX5K..."],
|
|
367
|
+
... created_at=datetime.now(timezone.utc)
|
|
368
|
+
... )
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
id: ArtifactID = Field(..., description="Unique artifact identifier (ULID)")
|
|
372
|
+
task_id: TaskID = Field(..., description="Parent task ID")
|
|
373
|
+
name: str = Field(..., description="Human-readable artifact name")
|
|
374
|
+
parts: list[PartID] = Field(..., description="Part IDs making up this artifact")
|
|
375
|
+
created_at: datetime = Field(..., description="Creation timestamp (UTC)")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class StateSnapshot(ASAPBaseModel):
|
|
379
|
+
"""First-class state persistence mechanism.
|
|
380
|
+
|
|
381
|
+
StateSnapshots enable task state to be saved and restored,
|
|
382
|
+
addressing a key limitation in other agent protocols. Supports
|
|
383
|
+
versioning and checkpoint flagging for important states.
|
|
384
|
+
|
|
385
|
+
Attributes:
|
|
386
|
+
id: Unique snapshot identifier (ULID format)
|
|
387
|
+
task_id: ID of the task this snapshot belongs to
|
|
388
|
+
version: Snapshot version number (auto-incremented)
|
|
389
|
+
data: Arbitrary state data (JSON-serializable dict)
|
|
390
|
+
checkpoint: Whether this is a significant checkpoint (default: False)
|
|
391
|
+
created_at: When the snapshot was created (UTC)
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
>>> from datetime import datetime, timezone
|
|
395
|
+
>>> snapshot = StateSnapshot(
|
|
396
|
+
... id="snap_01HX5K7R...",
|
|
397
|
+
... task_id="task_01HX5K4N...",
|
|
398
|
+
... version=3,
|
|
399
|
+
... data={"search_completed": True, "sources_analyzed": 15},
|
|
400
|
+
... checkpoint=True,
|
|
401
|
+
... created_at=datetime.now(timezone.utc)
|
|
402
|
+
... )
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
id: SnapshotID = Field(..., description="Unique snapshot identifier (ULID)")
|
|
406
|
+
task_id: TaskID = Field(..., description="Parent task ID")
|
|
407
|
+
version: int = Field(..., description="Snapshot version number", ge=1)
|
|
408
|
+
data: dict[str, Any] = Field(..., description="State data (JSON-serializable)")
|
|
409
|
+
checkpoint: bool = Field(default=False, description="Whether this is a significant checkpoint")
|
|
410
|
+
created_at: datetime = Field(..., description="Creation timestamp (UTC)")
|
asap/models/enums.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Enumerations for ASAP protocol.
|
|
2
|
+
|
|
3
|
+
This module defines all enum types used in the protocol to ensure
|
|
4
|
+
type safety and prevent magic strings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskStatus(str, Enum):
|
|
11
|
+
"""Task lifecycle states.
|
|
12
|
+
|
|
13
|
+
Tasks progress through these states during their lifecycle.
|
|
14
|
+
Terminal states are: COMPLETED, FAILED, CANCELLED.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> TaskStatus.COMPLETED.is_terminal()
|
|
18
|
+
True
|
|
19
|
+
>>> TaskStatus.WORKING.is_terminal()
|
|
20
|
+
False
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
SUBMITTED = "submitted"
|
|
24
|
+
WORKING = "working"
|
|
25
|
+
COMPLETED = "completed"
|
|
26
|
+
FAILED = "failed"
|
|
27
|
+
CANCELLED = "cancelled"
|
|
28
|
+
INPUT_REQUIRED = "input_required"
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def terminal_states(cls) -> frozenset["TaskStatus"]:
|
|
32
|
+
"""Return all terminal states.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Frozen set containing all terminal task states
|
|
36
|
+
"""
|
|
37
|
+
return frozenset({cls.COMPLETED, cls.FAILED, cls.CANCELLED})
|
|
38
|
+
|
|
39
|
+
def is_terminal(self) -> bool:
|
|
40
|
+
"""Check if this status represents a terminal state."""
|
|
41
|
+
return self in self.terminal_states()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MessageRole(str, Enum):
|
|
45
|
+
"""Message sender roles.
|
|
46
|
+
|
|
47
|
+
Defines the role of the entity sending a message in a conversation.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> MessageRole.USER.value
|
|
51
|
+
'user'
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
USER = "user"
|
|
55
|
+
ASSISTANT = "assistant"
|
|
56
|
+
SYSTEM = "system"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UpdateType(str, Enum):
|
|
60
|
+
"""Task update types.
|
|
61
|
+
|
|
62
|
+
Defines the type of update being sent for a task.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> UpdateType.PROGRESS.value
|
|
66
|
+
'progress'
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
PROGRESS = "progress"
|
|
70
|
+
INPUT_REQUIRED = "input_required"
|
|
71
|
+
STATUS_CHANGE = "status_change"
|
asap/models/envelope.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Envelope model for ASAP protocol messages.
|
|
2
|
+
|
|
3
|
+
The Envelope wraps all ASAP protocol messages, providing metadata for
|
|
4
|
+
routing, correlation, tracing, and versioning.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from asap.models.base import ASAPBaseModel
|
|
13
|
+
from asap.models.ids import generate_id
|
|
14
|
+
from asap.models.types import AgentURN
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Envelope(ASAPBaseModel):
|
|
18
|
+
"""ASAP protocol message envelope.
|
|
19
|
+
|
|
20
|
+
Envelope wraps all protocol messages with metadata for routing,
|
|
21
|
+
correlation, tracing, and versioning. Auto-generates id and timestamp
|
|
22
|
+
if not provided.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
id: Unique envelope identifier (auto-generated if not provided)
|
|
26
|
+
asap_version: ASAP protocol version (e.g., "0.1")
|
|
27
|
+
timestamp: Message timestamp in UTC (auto-generated if not provided)
|
|
28
|
+
sender: Sender agent URN
|
|
29
|
+
recipient: Recipient agent URN
|
|
30
|
+
payload_type: Type of payload (TaskRequest, TaskResponse, etc.)
|
|
31
|
+
payload: Actual message payload
|
|
32
|
+
correlation_id: Optional ID for correlating request/response pairs
|
|
33
|
+
trace_id: Optional ID for distributed tracing
|
|
34
|
+
extensions: Optional custom extensions
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> from datetime import datetime, timezone
|
|
38
|
+
>>> envelope = Envelope(
|
|
39
|
+
... asap_version="0.1",
|
|
40
|
+
... sender="urn:asap:agent:coordinator",
|
|
41
|
+
... recipient="urn:asap:agent:research-v1",
|
|
42
|
+
... payload_type="TaskRequest",
|
|
43
|
+
... payload={"conversation_id": "conv_123", "skill_id": "research", "input": {}}
|
|
44
|
+
... )
|
|
45
|
+
>>> # id and timestamp are auto-generated
|
|
46
|
+
>>> assert envelope.id is not None
|
|
47
|
+
>>> assert envelope.timestamp is not None
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
id: str | None = Field(
|
|
51
|
+
default=None, description="Unique envelope identifier (ULID, auto-generated)"
|
|
52
|
+
)
|
|
53
|
+
asap_version: str = Field(..., description="ASAP protocol version")
|
|
54
|
+
timestamp: datetime | None = Field(
|
|
55
|
+
default=None, description="Message timestamp (UTC, auto-generated)"
|
|
56
|
+
)
|
|
57
|
+
sender: AgentURN = Field(..., description="Sender agent URN")
|
|
58
|
+
recipient: AgentURN = Field(..., description="Recipient agent URN")
|
|
59
|
+
payload_type: str = Field(..., description="Payload type discriminator")
|
|
60
|
+
payload: dict[str, Any] = Field(..., description="Message payload")
|
|
61
|
+
correlation_id: str | None = Field(
|
|
62
|
+
default=None, description="Optional correlation ID for request/response pairing"
|
|
63
|
+
)
|
|
64
|
+
trace_id: str | None = Field(
|
|
65
|
+
default=None, description="Optional trace ID for distributed tracing"
|
|
66
|
+
)
|
|
67
|
+
extensions: dict[str, Any] | None = Field(
|
|
68
|
+
default=None, description="Optional custom extensions"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@field_validator("id", mode="before")
|
|
72
|
+
@classmethod
|
|
73
|
+
def generate_id_if_missing(cls, v: str | None) -> str:
|
|
74
|
+
"""Auto-generate ID if not provided."""
|
|
75
|
+
if v is None:
|
|
76
|
+
return generate_id()
|
|
77
|
+
return v
|
|
78
|
+
|
|
79
|
+
@field_validator("timestamp", mode="before")
|
|
80
|
+
@classmethod
|
|
81
|
+
def generate_timestamp_if_missing(cls, v: datetime | None) -> datetime:
|
|
82
|
+
"""Auto-generate timestamp if not provided."""
|
|
83
|
+
if v is None:
|
|
84
|
+
return datetime.now(timezone.utc)
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
@model_validator(mode="after")
|
|
88
|
+
def validate_response_correlation(self) -> "Envelope":
|
|
89
|
+
"""Validate that response payloads have correlation_id for request tracking."""
|
|
90
|
+
response_types = {"TaskResponse", "McpToolResult", "McpResourceData"}
|
|
91
|
+
|
|
92
|
+
if self.payload_type in response_types and not self.correlation_id:
|
|
93
|
+
raise ValueError(f"{self.payload_type} must have correlation_id for request tracking")
|
|
94
|
+
return self
|
asap/models/ids.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""ULID-based ID generation for ASAP protocol entities.
|
|
2
|
+
|
|
3
|
+
ULIDs (Universally Unique Lexicographically Sortable Identifiers) provide:
|
|
4
|
+
- 128-bit compatibility with UUID
|
|
5
|
+
- Lexicographic sorting by timestamp
|
|
6
|
+
- Canonically encoded as 26-character string (Crockford's Base32)
|
|
7
|
+
- Monotonically increasing within the same millisecond
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from ulid import ULID
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_id() -> str:
|
|
16
|
+
"""Generate a new ULID string.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A 26-character ULID string that is:
|
|
20
|
+
- Globally unique
|
|
21
|
+
- Lexicographically sortable by creation time
|
|
22
|
+
- URL-safe (uses Crockford's Base32 alphabet)
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> id1 = generate_id()
|
|
26
|
+
>>> len(id1)
|
|
27
|
+
26
|
|
28
|
+
>>> id2 = generate_id()
|
|
29
|
+
>>> id1 < id2 # Sortable by time
|
|
30
|
+
True
|
|
31
|
+
"""
|
|
32
|
+
return str(ULID())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def extract_timestamp(ulid: str) -> datetime:
|
|
36
|
+
"""Extract the timestamp from a ULID string.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
ulid: A 26-character ULID string
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A timezone-aware datetime in UTC representing when the ULID was created
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If the ULID string is invalid
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> ulid = generate_id()
|
|
49
|
+
>>> timestamp = extract_timestamp(ulid)
|
|
50
|
+
>>> timestamp.tzinfo == timezone.utc
|
|
51
|
+
True
|
|
52
|
+
"""
|
|
53
|
+
ulid_obj = ULID.from_str(ulid)
|
|
54
|
+
# ULID.datetime returns a timezone-aware datetime in UTC
|
|
55
|
+
return ulid_obj.datetime
|