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/schemas.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Schema export helpers for ASAP models.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for exporting JSON schemas from ASAP
|
|
4
|
+
Pydantic models, enabling schema validation and tooling integration.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from asap.schemas import get_schema_json, list_schema_entries
|
|
8
|
+
>>> schema = get_schema_json("agent")
|
|
9
|
+
>>> schema["title"]
|
|
10
|
+
'Agent'
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from asap.models import (
|
|
17
|
+
Agent,
|
|
18
|
+
Artifact,
|
|
19
|
+
ArtifactNotify,
|
|
20
|
+
Conversation,
|
|
21
|
+
DataPart,
|
|
22
|
+
Envelope,
|
|
23
|
+
FilePart,
|
|
24
|
+
Manifest,
|
|
25
|
+
McpResourceData,
|
|
26
|
+
McpResourceFetch,
|
|
27
|
+
McpToolCall,
|
|
28
|
+
McpToolResult,
|
|
29
|
+
Message,
|
|
30
|
+
MessageSend,
|
|
31
|
+
ResourcePart,
|
|
32
|
+
StateQuery,
|
|
33
|
+
StateRestore,
|
|
34
|
+
StateSnapshot,
|
|
35
|
+
Task,
|
|
36
|
+
TaskCancel,
|
|
37
|
+
TaskRequest,
|
|
38
|
+
TaskResponse,
|
|
39
|
+
TaskUpdate,
|
|
40
|
+
TemplatePart,
|
|
41
|
+
TextPart,
|
|
42
|
+
)
|
|
43
|
+
from asap.models.base import ASAPBaseModel
|
|
44
|
+
|
|
45
|
+
# Schema registry mapping names to model classes
|
|
46
|
+
SCHEMA_REGISTRY: dict[str, type[ASAPBaseModel]] = {
|
|
47
|
+
"agent": Agent,
|
|
48
|
+
"manifest": Manifest,
|
|
49
|
+
"conversation": Conversation,
|
|
50
|
+
"task": Task,
|
|
51
|
+
"message": Message,
|
|
52
|
+
"artifact": Artifact,
|
|
53
|
+
"state_snapshot": StateSnapshot,
|
|
54
|
+
"text_part": TextPart,
|
|
55
|
+
"data_part": DataPart,
|
|
56
|
+
"file_part": FilePart,
|
|
57
|
+
"resource_part": ResourcePart,
|
|
58
|
+
"template_part": TemplatePart,
|
|
59
|
+
"task_request": TaskRequest,
|
|
60
|
+
"task_response": TaskResponse,
|
|
61
|
+
"task_update": TaskUpdate,
|
|
62
|
+
"task_cancel": TaskCancel,
|
|
63
|
+
"message_send": MessageSend,
|
|
64
|
+
"state_query": StateQuery,
|
|
65
|
+
"state_restore": StateRestore,
|
|
66
|
+
"artifact_notify": ArtifactNotify,
|
|
67
|
+
"mcp_tool_call": McpToolCall,
|
|
68
|
+
"mcp_tool_result": McpToolResult,
|
|
69
|
+
"mcp_resource_fetch": McpResourceFetch,
|
|
70
|
+
"mcp_resource_data": McpResourceData,
|
|
71
|
+
"envelope": Envelope,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Total number of schemas in the registry
|
|
75
|
+
TOTAL_SCHEMA_COUNT = len(SCHEMA_REGISTRY)
|
|
76
|
+
|
|
77
|
+
# Schema output path mapping for directory organization
|
|
78
|
+
_SCHEMA_PATHS: dict[str, str] = {
|
|
79
|
+
# Entities
|
|
80
|
+
"agent": "entities/agent.schema.json",
|
|
81
|
+
"manifest": "entities/manifest.schema.json",
|
|
82
|
+
"conversation": "entities/conversation.schema.json",
|
|
83
|
+
"task": "entities/task.schema.json",
|
|
84
|
+
"message": "entities/message.schema.json",
|
|
85
|
+
"artifact": "entities/artifact.schema.json",
|
|
86
|
+
"state_snapshot": "entities/state_snapshot.schema.json",
|
|
87
|
+
# Parts
|
|
88
|
+
"text_part": "parts/text_part.schema.json",
|
|
89
|
+
"data_part": "parts/data_part.schema.json",
|
|
90
|
+
"file_part": "parts/file_part.schema.json",
|
|
91
|
+
"resource_part": "parts/resource_part.schema.json",
|
|
92
|
+
"template_part": "parts/template_part.schema.json",
|
|
93
|
+
# Payloads
|
|
94
|
+
"task_request": "payloads/task_request.schema.json",
|
|
95
|
+
"task_response": "payloads/task_response.schema.json",
|
|
96
|
+
"task_update": "payloads/task_update.schema.json",
|
|
97
|
+
"task_cancel": "payloads/task_cancel.schema.json",
|
|
98
|
+
"message_send": "payloads/message_send.schema.json",
|
|
99
|
+
"state_query": "payloads/state_query.schema.json",
|
|
100
|
+
"state_restore": "payloads/state_restore.schema.json",
|
|
101
|
+
"artifact_notify": "payloads/artifact_notify.schema.json",
|
|
102
|
+
"mcp_tool_call": "payloads/mcp_tool_call.schema.json",
|
|
103
|
+
"mcp_tool_result": "payloads/mcp_tool_result.schema.json",
|
|
104
|
+
"mcp_resource_fetch": "payloads/mcp_resource_fetch.schema.json",
|
|
105
|
+
"mcp_resource_data": "payloads/mcp_resource_data.schema.json",
|
|
106
|
+
# Envelope (root level)
|
|
107
|
+
"envelope": "envelope.schema.json",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def list_schema_entries(output_dir: Path) -> list[tuple[str, Path]]:
|
|
112
|
+
"""List all available schema names and their output paths.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
output_dir: Base directory where schemas are written.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of (schema_name, output_path) tuples.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> from pathlib import Path
|
|
122
|
+
>>> entries = list_schema_entries(Path("schemas"))
|
|
123
|
+
>>> any(name == "agent" for name, _ in entries)
|
|
124
|
+
True
|
|
125
|
+
"""
|
|
126
|
+
return [(name, output_dir / rel_path) for name, rel_path in _SCHEMA_PATHS.items()]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_schema_json(schema_name: str) -> dict[str, object]:
|
|
130
|
+
"""Return the JSON schema for a named model.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
schema_name: Schema identifier (e.g., "agent", "task_request").
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
JSON schema dictionary for the model.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If the schema name is not recognized.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> schema = get_schema_json("agent")
|
|
143
|
+
>>> schema["title"]
|
|
144
|
+
'Agent'
|
|
145
|
+
>>> "properties" in schema
|
|
146
|
+
True
|
|
147
|
+
"""
|
|
148
|
+
if schema_name not in SCHEMA_REGISTRY:
|
|
149
|
+
raise ValueError(f"Unknown schema name: {schema_name}")
|
|
150
|
+
return SCHEMA_REGISTRY[schema_name].model_json_schema()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def export_schema(model_class: type[ASAPBaseModel], output_path: Path) -> Path:
|
|
154
|
+
"""Export JSON Schema for a model to a file.
|
|
155
|
+
|
|
156
|
+
Creates parent directories if they don't exist. Overwrites existing files.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
model_class: Pydantic model class to export.
|
|
160
|
+
output_path: Path to write the schema file.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The path that was written.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> from pathlib import Path
|
|
167
|
+
>>> from asap.models import Agent
|
|
168
|
+
>>> path = export_schema(Agent, Path("/tmp/agent.schema.json"))
|
|
169
|
+
>>> path.exists()
|
|
170
|
+
True
|
|
171
|
+
"""
|
|
172
|
+
schema = model_class.model_json_schema()
|
|
173
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
output_path.write_text(json.dumps(schema, indent=2), encoding="utf-8")
|
|
175
|
+
return output_path
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def export_all_schemas(output_dir: Path) -> list[Path]:
|
|
179
|
+
"""Export all ASAP model schemas to the given directory.
|
|
180
|
+
|
|
181
|
+
Creates the directory structure (entities/, parts/, payloads/) and
|
|
182
|
+
writes all schema files.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
output_dir: Base directory to write schemas into.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of schema file paths that were written.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> from pathlib import Path
|
|
192
|
+
>>> paths = export_all_schemas(Path("/tmp/schemas"))
|
|
193
|
+
>>> len(paths) == 24
|
|
194
|
+
True
|
|
195
|
+
"""
|
|
196
|
+
written_paths: list[Path] = []
|
|
197
|
+
|
|
198
|
+
for name, rel_path in _SCHEMA_PATHS.items():
|
|
199
|
+
model_class = SCHEMA_REGISTRY[name]
|
|
200
|
+
output_path = output_dir / rel_path
|
|
201
|
+
written_paths.append(export_schema(model_class, output_path))
|
|
202
|
+
|
|
203
|
+
return written_paths
|
asap/state/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""ASAP State Management Module.
|
|
2
|
+
|
|
3
|
+
This module provides state machine functionality for managing
|
|
4
|
+
task lifecycles and state transitions in the ASAP protocol.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from asap.models.enums import TaskStatus
|
|
8
|
+
>>> can_transition(TaskStatus.SUBMITTED, TaskStatus.WORKING)
|
|
9
|
+
True
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .machine import can_transition, transition
|
|
13
|
+
from .snapshot import InMemorySnapshotStore, SnapshotStore
|
|
14
|
+
from asap.models.enums import TaskStatus
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"TaskStatus",
|
|
18
|
+
"can_transition",
|
|
19
|
+
"transition",
|
|
20
|
+
"SnapshotStore",
|
|
21
|
+
"InMemorySnapshotStore",
|
|
22
|
+
]
|
asap/state/machine.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""ASAP Task State Machine.
|
|
2
|
+
|
|
3
|
+
This module implements the task state machine for the ASAP protocol,
|
|
4
|
+
managing valid state transitions and providing transition validation.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from asap.models.enums import TaskStatus
|
|
8
|
+
>>> can_transition(TaskStatus.SUBMITTED, TaskStatus.WORKING)
|
|
9
|
+
True
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
from asap.errors import InvalidTransitionError
|
|
15
|
+
from asap.models.entities import Task
|
|
16
|
+
from asap.models.enums import TaskStatus
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Valid state transitions mapping
|
|
20
|
+
VALID_TRANSITIONS: dict[TaskStatus, set[TaskStatus]] = {
|
|
21
|
+
TaskStatus.SUBMITTED: {TaskStatus.WORKING, TaskStatus.CANCELLED},
|
|
22
|
+
TaskStatus.WORKING: {
|
|
23
|
+
TaskStatus.COMPLETED,
|
|
24
|
+
TaskStatus.FAILED,
|
|
25
|
+
TaskStatus.CANCELLED,
|
|
26
|
+
TaskStatus.INPUT_REQUIRED,
|
|
27
|
+
},
|
|
28
|
+
TaskStatus.INPUT_REQUIRED: {TaskStatus.WORKING, TaskStatus.CANCELLED},
|
|
29
|
+
TaskStatus.COMPLETED: set(), # Terminal state
|
|
30
|
+
TaskStatus.FAILED: set(), # Terminal state
|
|
31
|
+
TaskStatus.CANCELLED: set(), # Terminal state
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def can_transition(from_status: TaskStatus, to_status: TaskStatus) -> bool:
|
|
36
|
+
"""Check if a transition from one status to another is valid.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
from_status: Current task status
|
|
40
|
+
to_status: Target task status
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if the transition is valid, False otherwise
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> can_transition(TaskStatus.SUBMITTED, TaskStatus.WORKING)
|
|
47
|
+
True
|
|
48
|
+
>>> can_transition(TaskStatus.COMPLETED, TaskStatus.WORKING)
|
|
49
|
+
False
|
|
50
|
+
"""
|
|
51
|
+
valid_targets = VALID_TRANSITIONS.get(from_status, set())
|
|
52
|
+
return to_status in valid_targets
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def transition(task: Task, new_status: TaskStatus) -> Task:
|
|
56
|
+
"""Transition a task to a new status with validation.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
task: The task to transition
|
|
60
|
+
new_status: The target status
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
New task instance with updated status and updated_at timestamp
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
InvalidTransitionError: If the transition is not valid
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> task = Task(
|
|
70
|
+
... id="task_01HX5K4N...",
|
|
71
|
+
... conversation_id="conv_01HX5K3M...",
|
|
72
|
+
... status=TaskStatus.SUBMITTED,
|
|
73
|
+
... created_at=datetime.now(timezone.utc),
|
|
74
|
+
... updated_at=datetime.now(timezone.utc),
|
|
75
|
+
... )
|
|
76
|
+
>>> updated = transition(task, TaskStatus.WORKING)
|
|
77
|
+
>>> updated.status
|
|
78
|
+
<TaskStatus.WORKING: 'working'>
|
|
79
|
+
"""
|
|
80
|
+
if not can_transition(task.status, new_status):
|
|
81
|
+
raise InvalidTransitionError(
|
|
82
|
+
from_state=task.status.value, to_state=new_status.value, details={"task_id": task.id}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Create new task instance with updated status and timestamp (immutable approach)
|
|
86
|
+
return task.model_copy(update={"status": new_status, "updated_at": datetime.now(timezone.utc)})
|
asap/state/snapshot.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""ASAP Snapshot Store for task state persistence.
|
|
2
|
+
|
|
3
|
+
This module provides interfaces and implementations for storing and retrieving
|
|
4
|
+
task state snapshots, enabling state persistence across agent restarts.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> store = InMemorySnapshotStore()
|
|
8
|
+
>>> store.list_versions("task_01HX5K4N...")
|
|
9
|
+
[]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from asap.models.entities import StateSnapshot
|
|
16
|
+
from asap.models.types import TaskID
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class SnapshotStore(Protocol):
|
|
21
|
+
"""Protocol for snapshot storage implementations.
|
|
22
|
+
|
|
23
|
+
Provides the interface for storing and retrieving task state snapshots.
|
|
24
|
+
Implementations can use various backends (memory, database, file system, etc.).
|
|
25
|
+
This uses Protocol for duck typing, allowing any class that implements
|
|
26
|
+
these methods to be used as a SnapshotStore.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> class CustomStore:
|
|
30
|
+
... def save(self, snapshot: StateSnapshot) -> None:
|
|
31
|
+
... pass
|
|
32
|
+
... def get(self, task_id: TaskID, version: int | None = None) -> StateSnapshot | None:
|
|
33
|
+
... return None
|
|
34
|
+
... def list_versions(self, task_id: TaskID) -> list[int]:
|
|
35
|
+
... return []
|
|
36
|
+
... def delete(self, task_id: TaskID, version: int | None = None) -> bool:
|
|
37
|
+
... return False
|
|
38
|
+
>>> isinstance(CustomStore(), SnapshotStore)
|
|
39
|
+
True
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def save(self, snapshot: StateSnapshot) -> None:
|
|
43
|
+
"""Save a snapshot to the store.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
snapshot: The snapshot to save
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> from datetime import datetime, timezone
|
|
50
|
+
>>> store = InMemorySnapshotStore()
|
|
51
|
+
>>> snapshot = StateSnapshot(
|
|
52
|
+
... id="snap_01HX5K7R...",
|
|
53
|
+
... task_id="task_01HX5K4N...",
|
|
54
|
+
... version=1,
|
|
55
|
+
... data={"status": "submitted"},
|
|
56
|
+
... created_at=datetime.now(timezone.utc),
|
|
57
|
+
... )
|
|
58
|
+
>>> store.save(snapshot)
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def get(self, task_id: TaskID, version: int | None = None) -> StateSnapshot | None:
|
|
63
|
+
"""Retrieve a snapshot for the given task.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
task_id: The task ID to retrieve snapshots for
|
|
67
|
+
version: Optional specific version to retrieve. If None, returns latest.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The snapshot if found, None otherwise
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> store = InMemorySnapshotStore()
|
|
74
|
+
>>> store.get("task_01HX5K4N...")
|
|
75
|
+
None
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def list_versions(self, task_id: TaskID) -> list[int]:
|
|
80
|
+
"""List all available versions for a task.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
task_id: The task ID to list versions for
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of version numbers in ascending order
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
>>> store = InMemorySnapshotStore()
|
|
90
|
+
>>> store.list_versions("task_01HX5K4N...")
|
|
91
|
+
[]
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def delete(self, task_id: TaskID, version: int | None = None) -> bool:
|
|
96
|
+
"""Delete snapshot(s) for a task.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
task_id: The task ID
|
|
100
|
+
version: If provided, delete only this version. Otherwise delete all.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if any snapshots were deleted, False otherwise
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> store = InMemorySnapshotStore()
|
|
107
|
+
>>> store.delete("task_01HX5K4N...")
|
|
108
|
+
False
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class InMemorySnapshotStore:
|
|
114
|
+
"""In-memory implementation of SnapshotStore.
|
|
115
|
+
|
|
116
|
+
Stores snapshots in memory using dictionaries. Useful for testing
|
|
117
|
+
and simple applications that don't require persistence across restarts.
|
|
118
|
+
|
|
119
|
+
This implementation is thread-safe using RLock for concurrent access.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
"""Initialize the in-memory snapshot store.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> store = InMemorySnapshotStore()
|
|
127
|
+
>>> isinstance(store, InMemorySnapshotStore)
|
|
128
|
+
True
|
|
129
|
+
"""
|
|
130
|
+
self._lock = threading.RLock()
|
|
131
|
+
# task_id -> version -> snapshot
|
|
132
|
+
self._snapshots: dict[TaskID, dict[int, StateSnapshot]] = {}
|
|
133
|
+
# task_id -> latest version
|
|
134
|
+
self._latest_versions: dict[TaskID, int] = {}
|
|
135
|
+
|
|
136
|
+
def save(self, snapshot: StateSnapshot) -> None:
|
|
137
|
+
"""Save a snapshot to the in-memory store.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
snapshot: The snapshot to save
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> from datetime import datetime, timezone
|
|
144
|
+
>>> store = InMemorySnapshotStore()
|
|
145
|
+
>>> snapshot = StateSnapshot(
|
|
146
|
+
... id="snap_01HX5K7R...",
|
|
147
|
+
... task_id="task_01HX5K4N...",
|
|
148
|
+
... version=1,
|
|
149
|
+
... data={"status": "submitted"},
|
|
150
|
+
... created_at=datetime.now(timezone.utc),
|
|
151
|
+
... )
|
|
152
|
+
>>> store.save(snapshot)
|
|
153
|
+
"""
|
|
154
|
+
with self._lock:
|
|
155
|
+
task_id = snapshot.task_id
|
|
156
|
+
|
|
157
|
+
# Initialize storage for this task if needed
|
|
158
|
+
if task_id not in self._snapshots:
|
|
159
|
+
self._snapshots[task_id] = {}
|
|
160
|
+
|
|
161
|
+
# Store the snapshot
|
|
162
|
+
self._snapshots[task_id][snapshot.version] = snapshot
|
|
163
|
+
|
|
164
|
+
# Update latest version
|
|
165
|
+
self._latest_versions[task_id] = max(
|
|
166
|
+
self._latest_versions.get(task_id, 0), snapshot.version
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def get(self, task_id: TaskID, version: int | None = None) -> StateSnapshot | None:
|
|
170
|
+
"""Retrieve a snapshot from the in-memory store.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
task_id: The task ID to retrieve snapshots for
|
|
174
|
+
version: Optional specific version to retrieve. If None, returns latest.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The snapshot if found, None otherwise
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> store = InMemorySnapshotStore()
|
|
181
|
+
>>> store.get("task_01HX5K4N...")
|
|
182
|
+
None
|
|
183
|
+
"""
|
|
184
|
+
with self._lock:
|
|
185
|
+
if task_id not in self._snapshots:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
if version is None:
|
|
189
|
+
# Return latest version
|
|
190
|
+
latest_version = self._latest_versions.get(task_id)
|
|
191
|
+
if latest_version is None:
|
|
192
|
+
return None
|
|
193
|
+
return self._snapshots[task_id].get(latest_version)
|
|
194
|
+
|
|
195
|
+
# Return specific version
|
|
196
|
+
return self._snapshots[task_id].get(version)
|
|
197
|
+
|
|
198
|
+
def list_versions(self, task_id: TaskID) -> list[int]:
|
|
199
|
+
"""List all available versions for a task.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
task_id: The task ID to list versions for
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of version numbers in ascending order
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
>>> store = InMemorySnapshotStore()
|
|
209
|
+
>>> store.list_versions("task_01HX5K4N...")
|
|
210
|
+
[]
|
|
211
|
+
"""
|
|
212
|
+
with self._lock:
|
|
213
|
+
if task_id not in self._snapshots:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
return sorted(self._snapshots[task_id].keys())
|
|
217
|
+
|
|
218
|
+
def delete(self, task_id: TaskID, version: int | None = None) -> bool:
|
|
219
|
+
"""Delete snapshot(s) for a task.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
task_id: The task ID
|
|
223
|
+
version: If provided, delete only this version. Otherwise delete all.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if any snapshots were deleted, False otherwise
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
>>> store = InMemorySnapshotStore()
|
|
230
|
+
>>> store.delete("task_01HX5K4N...")
|
|
231
|
+
False
|
|
232
|
+
"""
|
|
233
|
+
with self._lock:
|
|
234
|
+
if task_id not in self._snapshots:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
if version is None:
|
|
238
|
+
# Delete all versions for this task
|
|
239
|
+
if task_id in self._snapshots:
|
|
240
|
+
del self._snapshots[task_id]
|
|
241
|
+
if task_id in self._latest_versions:
|
|
242
|
+
del self._latest_versions[task_id]
|
|
243
|
+
return True
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
# Delete specific version
|
|
247
|
+
if version in self._snapshots[task_id]:
|
|
248
|
+
del self._snapshots[task_id][version]
|
|
249
|
+
|
|
250
|
+
# Update latest version if needed
|
|
251
|
+
if self._latest_versions.get(task_id) == version:
|
|
252
|
+
if self._snapshots[task_id]:
|
|
253
|
+
self._latest_versions[task_id] = max(self._snapshots[task_id].keys())
|
|
254
|
+
else:
|
|
255
|
+
del self._latest_versions[task_id]
|
|
256
|
+
|
|
257
|
+
# Clean up empty task dict
|
|
258
|
+
if not self._snapshots[task_id]:
|
|
259
|
+
del self._snapshots[task_id]
|
|
260
|
+
if task_id in self._latest_versions:
|
|
261
|
+
del self._latest_versions[task_id]
|
|
262
|
+
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
return False
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""ASAP Protocol HTTP Transport Layer.
|
|
2
|
+
|
|
3
|
+
This module provides HTTP-based transport for ASAP messages using:
|
|
4
|
+
- JSON-RPC 2.0 for request/response wrapping
|
|
5
|
+
- FastAPI for server implementation
|
|
6
|
+
- httpx for async client implementation
|
|
7
|
+
- Handler registry for payload dispatch
|
|
8
|
+
|
|
9
|
+
Public exports:
|
|
10
|
+
JsonRpcRequest: JSON-RPC 2.0 request wrapper
|
|
11
|
+
JsonRpcResponse: JSON-RPC 2.0 response wrapper
|
|
12
|
+
JsonRpcError: JSON-RPC 2.0 error object
|
|
13
|
+
JsonRpcErrorResponse: JSON-RPC 2.0 error response wrapper
|
|
14
|
+
create_app: FastAPI application factory
|
|
15
|
+
HandlerRegistry: Registry for payload handlers
|
|
16
|
+
HandlerNotFoundError: Error for missing handlers
|
|
17
|
+
Handler: Type alias for handler functions
|
|
18
|
+
create_echo_handler: Factory for echo handler
|
|
19
|
+
create_default_registry: Factory for default registry
|
|
20
|
+
ASAPClient: Async HTTP client for agent communication
|
|
21
|
+
ASAPConnectionError: Connection error exception
|
|
22
|
+
ASAPTimeoutError: Timeout error exception
|
|
23
|
+
ASAPRemoteError: Remote error exception
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> from asap.transport import ASAPClient, create_app
|
|
27
|
+
>>> from asap.models.entities import Manifest, Capability, Endpoint, Skill
|
|
28
|
+
>>> manifest = Manifest(
|
|
29
|
+
... id="urn:asap:agent:demo",
|
|
30
|
+
... name="Demo Agent",
|
|
31
|
+
... version="1.0.0",
|
|
32
|
+
... description="Demo manifest",
|
|
33
|
+
... capabilities=Capability(
|
|
34
|
+
... asap_version="0.1",
|
|
35
|
+
... skills=[Skill(id="echo", description="Echo")],
|
|
36
|
+
... state_persistence=False,
|
|
37
|
+
... ),
|
|
38
|
+
... endpoints=Endpoint(asap="http://localhost:8000/asap"),
|
|
39
|
+
... )
|
|
40
|
+
>>> app = create_app(manifest)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from asap.transport.client import (
|
|
44
|
+
ASAPClient,
|
|
45
|
+
ASAPConnectionError,
|
|
46
|
+
ASAPRemoteError,
|
|
47
|
+
ASAPTimeoutError,
|
|
48
|
+
)
|
|
49
|
+
from asap.transport.handlers import (
|
|
50
|
+
Handler,
|
|
51
|
+
HandlerNotFoundError,
|
|
52
|
+
HandlerRegistry,
|
|
53
|
+
create_default_registry,
|
|
54
|
+
create_echo_handler,
|
|
55
|
+
)
|
|
56
|
+
from asap.transport.jsonrpc import (
|
|
57
|
+
JsonRpcError,
|
|
58
|
+
JsonRpcErrorResponse,
|
|
59
|
+
JsonRpcRequest,
|
|
60
|
+
JsonRpcResponse,
|
|
61
|
+
)
|
|
62
|
+
from asap.transport.server import ASAPRequestHandler, create_app
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
# JSON-RPC
|
|
66
|
+
"JsonRpcRequest",
|
|
67
|
+
"JsonRpcResponse",
|
|
68
|
+
"JsonRpcError",
|
|
69
|
+
"JsonRpcErrorResponse",
|
|
70
|
+
# Server
|
|
71
|
+
"create_app",
|
|
72
|
+
"ASAPRequestHandler",
|
|
73
|
+
# Handlers
|
|
74
|
+
"HandlerRegistry",
|
|
75
|
+
"HandlerNotFoundError",
|
|
76
|
+
"Handler",
|
|
77
|
+
"create_echo_handler",
|
|
78
|
+
"create_default_registry",
|
|
79
|
+
# Client
|
|
80
|
+
"ASAPClient",
|
|
81
|
+
"ASAPConnectionError",
|
|
82
|
+
"ASAPTimeoutError",
|
|
83
|
+
"ASAPRemoteError",
|
|
84
|
+
]
|