jaf-py 2.5.10__py3-none-any.whl → 2.5.11__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.
- jaf/__init__.py +154 -57
- jaf/a2a/__init__.py +42 -21
- jaf/a2a/agent.py +79 -126
- jaf/a2a/agent_card.py +87 -78
- jaf/a2a/client.py +30 -66
- jaf/a2a/examples/client_example.py +12 -12
- jaf/a2a/examples/integration_example.py +38 -47
- jaf/a2a/examples/server_example.py +56 -53
- jaf/a2a/memory/__init__.py +0 -4
- jaf/a2a/memory/cleanup.py +28 -21
- jaf/a2a/memory/factory.py +155 -133
- jaf/a2a/memory/providers/composite.py +21 -26
- jaf/a2a/memory/providers/in_memory.py +89 -83
- jaf/a2a/memory/providers/postgres.py +117 -115
- jaf/a2a/memory/providers/redis.py +128 -121
- jaf/a2a/memory/serialization.py +77 -87
- jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
- jaf/a2a/memory/tests/test_cleanup.py +211 -94
- jaf/a2a/memory/tests/test_serialization.py +73 -68
- jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
- jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
- jaf/a2a/memory/types.py +91 -53
- jaf/a2a/protocol.py +95 -125
- jaf/a2a/server.py +90 -118
- jaf/a2a/standalone_client.py +30 -43
- jaf/a2a/tests/__init__.py +16 -33
- jaf/a2a/tests/run_tests.py +17 -53
- jaf/a2a/tests/test_agent.py +40 -140
- jaf/a2a/tests/test_client.py +54 -117
- jaf/a2a/tests/test_integration.py +28 -82
- jaf/a2a/tests/test_protocol.py +54 -139
- jaf/a2a/tests/test_types.py +50 -136
- jaf/a2a/types.py +58 -34
- jaf/cli.py +21 -41
- jaf/core/__init__.py +7 -1
- jaf/core/agent_tool.py +93 -72
- jaf/core/analytics.py +257 -207
- jaf/core/checkpoint.py +223 -0
- jaf/core/composition.py +249 -235
- jaf/core/engine.py +817 -519
- jaf/core/errors.py +55 -42
- jaf/core/guardrails.py +276 -202
- jaf/core/handoff.py +47 -31
- jaf/core/parallel_agents.py +69 -75
- jaf/core/performance.py +75 -73
- jaf/core/proxy.py +43 -44
- jaf/core/proxy_helpers.py +24 -27
- jaf/core/regeneration.py +220 -129
- jaf/core/state.py +68 -66
- jaf/core/streaming.py +115 -108
- jaf/core/tool_results.py +111 -101
- jaf/core/tools.py +114 -116
- jaf/core/tracing.py +269 -210
- jaf/core/types.py +371 -151
- jaf/core/workflows.py +209 -168
- jaf/exceptions.py +46 -38
- jaf/memory/__init__.py +1 -6
- jaf/memory/approval_storage.py +54 -77
- jaf/memory/factory.py +4 -4
- jaf/memory/providers/in_memory.py +216 -180
- jaf/memory/providers/postgres.py +216 -146
- jaf/memory/providers/redis.py +173 -116
- jaf/memory/types.py +70 -51
- jaf/memory/utils.py +36 -34
- jaf/plugins/__init__.py +12 -12
- jaf/plugins/base.py +105 -96
- jaf/policies/__init__.py +0 -1
- jaf/policies/handoff.py +37 -46
- jaf/policies/validation.py +76 -52
- jaf/providers/__init__.py +6 -3
- jaf/providers/mcp.py +97 -51
- jaf/providers/model.py +360 -279
- jaf/server/__init__.py +1 -1
- jaf/server/main.py +7 -11
- jaf/server/server.py +514 -359
- jaf/server/types.py +208 -52
- jaf/utils/__init__.py +17 -18
- jaf/utils/attachments.py +111 -116
- jaf/utils/document_processor.py +175 -174
- jaf/visualization/__init__.py +1 -1
- jaf/visualization/example.py +111 -110
- jaf/visualization/functional_core.py +46 -71
- jaf/visualization/graphviz.py +154 -189
- jaf/visualization/imperative_shell.py +7 -16
- jaf/visualization/types.py +8 -4
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
- jaf_py-2.5.11.dist-info/RECORD +97 -0
- jaf_py-2.5.10.dist-info/RECORD +0 -96
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
|
@@ -36,12 +36,14 @@ from jaf.a2a.types import (
|
|
|
36
36
|
# Import other providers when they're available
|
|
37
37
|
try:
|
|
38
38
|
from jaf.a2a.memory.providers.redis import create_a2a_redis_task_provider
|
|
39
|
+
|
|
39
40
|
REDIS_AVAILABLE = True
|
|
40
41
|
except ImportError:
|
|
41
42
|
REDIS_AVAILABLE = False
|
|
42
43
|
|
|
43
44
|
try:
|
|
44
45
|
from jaf.a2a.memory.providers.postgres import create_a2a_postgres_task_provider
|
|
46
|
+
|
|
45
47
|
POSTGRES_AVAILABLE = True
|
|
46
48
|
except ImportError:
|
|
47
49
|
POSTGRES_AVAILABLE = False
|
|
@@ -51,9 +53,7 @@ class TaskLifecycleTestBase:
|
|
|
51
53
|
"""Base test class with helper methods for task lifecycle testing"""
|
|
52
54
|
|
|
53
55
|
def create_submission_task(
|
|
54
|
-
self,
|
|
55
|
-
task_id: str = "lifecycle_task_001",
|
|
56
|
-
context_id: str = "lifecycle_ctx_001"
|
|
56
|
+
self, task_id: str = "lifecycle_task_001", context_id: str = "lifecycle_ctx_001"
|
|
57
57
|
) -> A2ATask:
|
|
58
58
|
"""Create a task in submitted state"""
|
|
59
59
|
return A2ATask(
|
|
@@ -67,43 +67,36 @@ class TaskLifecycleTestBase:
|
|
|
67
67
|
parts=[A2ATextPart(kind="text", text="Please help me with this task")],
|
|
68
68
|
messageId=f"submit_{task_id}",
|
|
69
69
|
contextId=context_id,
|
|
70
|
-
kind="message"
|
|
70
|
+
kind="message",
|
|
71
71
|
),
|
|
72
|
-
timestamp=datetime.now(timezone.utc).isoformat()
|
|
72
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
73
73
|
),
|
|
74
|
-
metadata={
|
|
75
|
-
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
76
|
-
"priority": "normal"
|
|
77
|
-
}
|
|
74
|
+
metadata={"created_at": datetime.now(timezone.utc).isoformat(), "priority": "normal"},
|
|
78
75
|
)
|
|
79
76
|
|
|
80
77
|
def create_working_task_update(
|
|
81
|
-
self,
|
|
82
|
-
base_task: A2ATask,
|
|
83
|
-
progress_message: str = "Processing your request..."
|
|
78
|
+
self, base_task: A2ATask, progress_message: str = "Processing your request..."
|
|
84
79
|
) -> A2ATask:
|
|
85
80
|
"""Create task update transitioning to working state"""
|
|
86
|
-
return base_task.model_copy(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
return base_task.model_copy(
|
|
82
|
+
update={
|
|
83
|
+
"status": A2ATaskStatus(
|
|
84
|
+
state=TaskState.WORKING,
|
|
85
|
+
message=A2AMessage(
|
|
86
|
+
role="agent",
|
|
87
|
+
parts=[A2ATextPart(kind="text", text=progress_message)],
|
|
88
|
+
messageId=f"working_{base_task.id}",
|
|
89
|
+
contextId=base_task.context_id,
|
|
90
|
+
kind="message",
|
|
91
|
+
),
|
|
92
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
95
93
|
),
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
base_task.status.message
|
|
100
|
-
] if base_task.status.message else []
|
|
101
|
-
})
|
|
94
|
+
"history": [base_task.status.message] if base_task.status.message else [],
|
|
95
|
+
}
|
|
96
|
+
)
|
|
102
97
|
|
|
103
98
|
def create_completed_task_update(
|
|
104
|
-
self,
|
|
105
|
-
working_task: A2ATask,
|
|
106
|
-
result_text: str = "Task completed successfully"
|
|
99
|
+
self, working_task: A2ATask, result_text: str = "Task completed successfully"
|
|
107
100
|
) -> A2ATask:
|
|
108
101
|
"""Create task update transitioning to completed state with artifacts"""
|
|
109
102
|
completion_message = A2AMessage(
|
|
@@ -111,7 +104,7 @@ class TaskLifecycleTestBase:
|
|
|
111
104
|
parts=[A2ATextPart(kind="text", text=result_text)],
|
|
112
105
|
messageId=f"complete_{working_task.id}",
|
|
113
106
|
contextId=working_task.context_id,
|
|
114
|
-
kind="message"
|
|
107
|
+
kind="message",
|
|
115
108
|
)
|
|
116
109
|
|
|
117
110
|
result_artifact = A2AArtifact(
|
|
@@ -120,8 +113,11 @@ class TaskLifecycleTestBase:
|
|
|
120
113
|
description="Final result of the completed task",
|
|
121
114
|
parts=[
|
|
122
115
|
A2ATextPart(kind="text", text="Here is your completed result."),
|
|
123
|
-
A2ADataPart(
|
|
124
|
-
|
|
116
|
+
A2ADataPart(
|
|
117
|
+
kind="data",
|
|
118
|
+
data={"success": True, "timestamp": datetime.now(timezone.utc).isoformat()},
|
|
119
|
+
),
|
|
120
|
+
],
|
|
125
121
|
)
|
|
126
122
|
|
|
127
123
|
# Build complete history
|
|
@@ -129,20 +125,20 @@ class TaskLifecycleTestBase:
|
|
|
129
125
|
if working_task.status.message:
|
|
130
126
|
history.append(working_task.status.message)
|
|
131
127
|
|
|
132
|
-
return working_task.model_copy(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
return working_task.model_copy(
|
|
129
|
+
update={
|
|
130
|
+
"status": A2ATaskStatus(
|
|
131
|
+
state=TaskState.COMPLETED,
|
|
132
|
+
message=completion_message,
|
|
133
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
134
|
+
),
|
|
135
|
+
"history": history,
|
|
136
|
+
"artifacts": [result_artifact],
|
|
137
|
+
}
|
|
138
|
+
)
|
|
141
139
|
|
|
142
140
|
def create_failed_task_update(
|
|
143
|
-
self,
|
|
144
|
-
working_task: A2ATask,
|
|
145
|
-
error_message: str = "Task failed due to an error"
|
|
141
|
+
self, working_task: A2ATask, error_message: str = "Task failed due to an error"
|
|
146
142
|
) -> A2ATask:
|
|
147
143
|
"""Create task update transitioning to failed state"""
|
|
148
144
|
failure_message = A2AMessage(
|
|
@@ -150,26 +146,26 @@ class TaskLifecycleTestBase:
|
|
|
150
146
|
parts=[A2ATextPart(kind="text", text=error_message)],
|
|
151
147
|
messageId=f"failed_{working_task.id}",
|
|
152
148
|
contextId=working_task.context_id,
|
|
153
|
-
kind="message"
|
|
149
|
+
kind="message",
|
|
154
150
|
)
|
|
155
151
|
|
|
156
152
|
history = list(working_task.history or [])
|
|
157
153
|
if working_task.status.message:
|
|
158
154
|
history.append(working_task.status.message)
|
|
159
155
|
|
|
160
|
-
return working_task.model_copy(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
return working_task.model_copy(
|
|
157
|
+
update={
|
|
158
|
+
"status": A2ATaskStatus(
|
|
159
|
+
state=TaskState.FAILED,
|
|
160
|
+
message=failure_message,
|
|
161
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
162
|
+
),
|
|
163
|
+
"history": history,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
168
166
|
|
|
169
167
|
def create_canceled_task_update(
|
|
170
|
-
self,
|
|
171
|
-
working_task: A2ATask,
|
|
172
|
-
cancel_reason: str = "Task was canceled by user"
|
|
168
|
+
self, working_task: A2ATask, cancel_reason: str = "Task was canceled by user"
|
|
173
169
|
) -> A2ATask:
|
|
174
170
|
"""Create task update transitioning to canceled state"""
|
|
175
171
|
cancel_message = A2AMessage(
|
|
@@ -177,28 +173,35 @@ class TaskLifecycleTestBase:
|
|
|
177
173
|
parts=[A2ATextPart(kind="text", text=cancel_reason)],
|
|
178
174
|
messageId=f"cancel_{working_task.id}",
|
|
179
175
|
contextId=working_task.context_id,
|
|
180
|
-
kind="message"
|
|
176
|
+
kind="message",
|
|
181
177
|
)
|
|
182
178
|
|
|
183
179
|
history = list(working_task.history or [])
|
|
184
180
|
if working_task.status.message:
|
|
185
181
|
history.append(working_task.status.message)
|
|
186
182
|
|
|
187
|
-
return working_task.model_copy(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
183
|
+
return working_task.model_copy(
|
|
184
|
+
update={
|
|
185
|
+
"status": A2ATaskStatus(
|
|
186
|
+
state=TaskState.CANCELED,
|
|
187
|
+
message=cancel_message,
|
|
188
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
189
|
+
),
|
|
190
|
+
"history": history,
|
|
191
|
+
}
|
|
192
|
+
)
|
|
195
193
|
|
|
196
194
|
|
|
197
195
|
# Provider parameter list for running tests across all providers
|
|
198
196
|
PROVIDER_TYPES = [
|
|
199
197
|
"in_memory",
|
|
200
|
-
pytest.param(
|
|
201
|
-
|
|
198
|
+
pytest.param(
|
|
199
|
+
"redis", marks=pytest.mark.skipif(not REDIS_AVAILABLE, reason="Redis not available")
|
|
200
|
+
),
|
|
201
|
+
pytest.param(
|
|
202
|
+
"postgres",
|
|
203
|
+
marks=pytest.mark.skipif(not POSTGRES_AVAILABLE, reason="PostgreSQL not available"),
|
|
204
|
+
),
|
|
202
205
|
]
|
|
203
206
|
|
|
204
207
|
|
|
@@ -220,21 +223,25 @@ async def provider(request):
|
|
|
220
223
|
port=6379,
|
|
221
224
|
db=15, # Use separate DB for testing
|
|
222
225
|
key_prefix="jaf_test:a2a:tasks:",
|
|
223
|
-
password="12345678"
|
|
226
|
+
password="12345678",
|
|
224
227
|
)
|
|
225
228
|
try:
|
|
226
229
|
redis_client = redis.Redis(
|
|
227
|
-
host=config.host,
|
|
228
|
-
port=config.port,
|
|
229
|
-
db=config.db,
|
|
230
|
-
password=config.password,
|
|
231
|
-
decode_responses=True
|
|
230
|
+
host=config.host,
|
|
231
|
+
port=config.port,
|
|
232
|
+
db=config.db,
|
|
233
|
+
password=config.password,
|
|
234
|
+
decode_responses=True,
|
|
232
235
|
)
|
|
233
236
|
await redis_client.ping()
|
|
234
237
|
await redis_client.flushdb()
|
|
235
238
|
p_result = await create_a2a_redis_task_provider(config, redis_client)
|
|
236
239
|
p = p_result.data
|
|
237
|
-
except (
|
|
240
|
+
except (
|
|
241
|
+
redis.exceptions.ConnectionError,
|
|
242
|
+
ConnectionRefusedError,
|
|
243
|
+
redis.exceptions.AuthenticationError,
|
|
244
|
+
) as e:
|
|
238
245
|
pytest.skip(f"Redis not available at {config.host}:{config.port}: {e}")
|
|
239
246
|
|
|
240
247
|
elif provider_type == "postgres":
|
|
@@ -243,7 +250,7 @@ async def provider(request):
|
|
|
243
250
|
port=5432,
|
|
244
251
|
database="jaf_test",
|
|
245
252
|
username="postgres",
|
|
246
|
-
table_name="a2a_tasks_test"
|
|
253
|
+
table_name="a2a_tasks_test",
|
|
247
254
|
)
|
|
248
255
|
try:
|
|
249
256
|
pg_pool = await asyncpg.create_pool(
|
|
@@ -265,7 +272,7 @@ async def provider(request):
|
|
|
265
272
|
await redis_client.aclose()
|
|
266
273
|
if pg_pool:
|
|
267
274
|
await pg_pool.close()
|
|
268
|
-
elif provider_type not in [
|
|
275
|
+
elif provider_type not in ["redis", "postgres"]: # Don't fail if skipped
|
|
269
276
|
pytest.fail(f"Unknown provider type or provider failed to initialize: {provider_type}")
|
|
270
277
|
|
|
271
278
|
|
|
@@ -293,7 +300,9 @@ class TestTaskLifecycleHappyPath(TaskLifecycleTestBase):
|
|
|
293
300
|
assert stored_task.status.state == TaskState.SUBMITTED
|
|
294
301
|
|
|
295
302
|
# Step 2: Transition to working state
|
|
296
|
-
working_task = self.create_working_task_update(
|
|
303
|
+
working_task = self.create_working_task_update(
|
|
304
|
+
stored_task, "Starting to work on your request..."
|
|
305
|
+
)
|
|
297
306
|
|
|
298
307
|
update_result = await provider.update_task(working_task)
|
|
299
308
|
assert update_result.data is None, "Update should succeed"
|
|
@@ -314,13 +323,15 @@ class TestTaskLifecycleHappyPath(TaskLifecycleTestBase):
|
|
|
314
323
|
parts=[A2ATextPart(kind="text", text="50% complete...")],
|
|
315
324
|
messageId="progress_001",
|
|
316
325
|
contextId="happy_ctx_001",
|
|
317
|
-
kind="message"
|
|
318
|
-
)
|
|
326
|
+
kind="message",
|
|
327
|
+
),
|
|
319
328
|
)
|
|
320
329
|
assert intermediate_update_result.data is None, "Status update should succeed"
|
|
321
330
|
|
|
322
331
|
# Step 4: Transition to completed state
|
|
323
|
-
completed_task = self.create_completed_task_update(
|
|
332
|
+
completed_task = self.create_completed_task_update(
|
|
333
|
+
working_stored, "Your task has been completed successfully!"
|
|
334
|
+
)
|
|
324
335
|
|
|
325
336
|
complete_result = await provider.update_task(completed_task)
|
|
326
337
|
assert complete_result.data is None, "Completion should succeed"
|
|
@@ -365,7 +376,7 @@ class TestTaskLifecycleHappyPath(TaskLifecycleTestBase):
|
|
|
365
376
|
"25% complete - analyzing request",
|
|
366
377
|
"50% complete - processing data",
|
|
367
378
|
"75% complete - generating results",
|
|
368
|
-
"90% complete - finalizing output"
|
|
379
|
+
"90% complete - finalizing output",
|
|
369
380
|
]
|
|
370
381
|
|
|
371
382
|
for i, message in enumerate(progress_messages):
|
|
@@ -374,13 +385,11 @@ class TestTaskLifecycleHappyPath(TaskLifecycleTestBase):
|
|
|
374
385
|
parts=[A2ATextPart(kind="text", text=message)],
|
|
375
386
|
messageId=f"progress_{i}",
|
|
376
387
|
contextId="multi_ctx_001",
|
|
377
|
-
kind="message"
|
|
388
|
+
kind="message",
|
|
378
389
|
)
|
|
379
390
|
|
|
380
391
|
update_result = await provider.update_task_status(
|
|
381
|
-
"multi_001",
|
|
382
|
-
TaskState.WORKING,
|
|
383
|
-
status_message
|
|
392
|
+
"multi_001", TaskState.WORKING, status_message
|
|
384
393
|
)
|
|
385
394
|
assert update_result.data is None, f"Progress update {i} should succeed"
|
|
386
395
|
|
|
@@ -413,7 +422,9 @@ class TestTaskLifecycleUnhappyPath(TaskLifecycleTestBase):
|
|
|
413
422
|
await provider.update_task(working_task)
|
|
414
423
|
|
|
415
424
|
# Transition to failed state
|
|
416
|
-
failed_task = self.create_failed_task_update(
|
|
425
|
+
failed_task = self.create_failed_task_update(
|
|
426
|
+
working_task, "Encountered an unexpected error during processing"
|
|
427
|
+
)
|
|
417
428
|
|
|
418
429
|
fail_result = await provider.update_task(failed_task)
|
|
419
430
|
assert fail_result.data is None, "Failure update should succeed"
|
|
@@ -454,12 +465,14 @@ class TestTaskLifecycleUnhappyPath(TaskLifecycleTestBase):
|
|
|
454
465
|
parts=[A2ATextPart(kind="text", text="Working on your request...")],
|
|
455
466
|
messageId="working_msg",
|
|
456
467
|
contextId="cancel_ctx_001",
|
|
457
|
-
kind="message"
|
|
458
|
-
)
|
|
468
|
+
kind="message",
|
|
469
|
+
),
|
|
459
470
|
)
|
|
460
471
|
|
|
461
472
|
# Cancel the task
|
|
462
|
-
canceled_task = self.create_canceled_task_update(
|
|
473
|
+
canceled_task = self.create_canceled_task_update(
|
|
474
|
+
working_task, "Task was canceled at user request"
|
|
475
|
+
)
|
|
463
476
|
|
|
464
477
|
cancel_result = await provider.update_task(canceled_task)
|
|
465
478
|
assert cancel_result.data is None, "Cancellation should succeed"
|
|
@@ -498,7 +511,13 @@ class TestMultiTaskContextManagement(TaskLifecycleTestBase):
|
|
|
498
511
|
assert store_result.data is None, f"Task {task_id} should store successfully"
|
|
499
512
|
|
|
500
513
|
# Progress tasks to different states
|
|
501
|
-
states = [
|
|
514
|
+
states = [
|
|
515
|
+
TaskState.SUBMITTED,
|
|
516
|
+
TaskState.WORKING,
|
|
517
|
+
TaskState.COMPLETED,
|
|
518
|
+
TaskState.FAILED,
|
|
519
|
+
TaskState.CANCELED,
|
|
520
|
+
]
|
|
502
521
|
|
|
503
522
|
for i, (task_id, target_state) in enumerate(zip(task_ids, states)):
|
|
504
523
|
get_result = await provider.get_task(task_id)
|
|
@@ -667,11 +686,7 @@ class TestTaskQueryAndPagination(TaskLifecycleTestBase):
|
|
|
667
686
|
offset = 0
|
|
668
687
|
|
|
669
688
|
while True:
|
|
670
|
-
query = A2ATaskQuery(
|
|
671
|
-
context_id=context_id,
|
|
672
|
-
limit=page_size,
|
|
673
|
-
offset=offset
|
|
674
|
-
)
|
|
689
|
+
query = A2ATaskQuery(context_id=context_id, limit=page_size, offset=offset)
|
|
675
690
|
|
|
676
691
|
page_result = await provider.find_tasks(query)
|
|
677
692
|
assert page_result.data is not None
|
|
@@ -688,7 +703,9 @@ class TestTaskQueryAndPagination(TaskLifecycleTestBase):
|
|
|
688
703
|
break
|
|
689
704
|
|
|
690
705
|
# Verify we got all tasks
|
|
691
|
-
assert len(all_retrieved) == total_tasks,
|
|
706
|
+
assert len(all_retrieved) == total_tasks, (
|
|
707
|
+
f"Expected {total_tasks} tasks, got {len(all_retrieved)}"
|
|
708
|
+
)
|
|
692
709
|
|
|
693
710
|
# Verify no duplicates
|
|
694
711
|
task_ids = [task.id for task in all_retrieved]
|
|
@@ -708,30 +725,27 @@ class TestTaskQueryAndPagination(TaskLifecycleTestBase):
|
|
|
708
725
|
# Tasks from 1 hour ago
|
|
709
726
|
old_time = base_time - timedelta(hours=1)
|
|
710
727
|
old_task = self.create_submission_task("old_task", context_id)
|
|
711
|
-
old_task = old_task.model_copy(
|
|
712
|
-
|
|
713
|
-
"timestamp": old_time.isoformat()
|
|
714
|
-
}
|
|
715
|
-
|
|
728
|
+
old_task = old_task.model_copy(
|
|
729
|
+
update={
|
|
730
|
+
"status": old_task.status.model_copy(update={"timestamp": old_time.isoformat()})
|
|
731
|
+
}
|
|
732
|
+
)
|
|
716
733
|
# Store with created_at metadata to control the timestamp used for filtering
|
|
717
734
|
await provider.store_task(old_task, metadata={"created_at": old_time.isoformat()})
|
|
718
735
|
|
|
719
736
|
# Tasks from now
|
|
720
737
|
new_task = self.create_submission_task("new_task", context_id)
|
|
721
|
-
new_task = new_task.model_copy(
|
|
722
|
-
|
|
723
|
-
"timestamp": base_time.isoformat()
|
|
724
|
-
}
|
|
725
|
-
|
|
738
|
+
new_task = new_task.model_copy(
|
|
739
|
+
update={
|
|
740
|
+
"status": new_task.status.model_copy(update={"timestamp": base_time.isoformat()})
|
|
741
|
+
}
|
|
742
|
+
)
|
|
726
743
|
# Store with created_at metadata to control the timestamp used for filtering
|
|
727
744
|
await provider.store_task(new_task, metadata={"created_at": base_time.isoformat()})
|
|
728
745
|
|
|
729
746
|
# Query tasks since 30 minutes ago
|
|
730
747
|
since_time = base_time - timedelta(minutes=30)
|
|
731
|
-
time_query = A2ATaskQuery(
|
|
732
|
-
context_id=context_id,
|
|
733
|
-
since=since_time
|
|
734
|
-
)
|
|
748
|
+
time_query = A2ATaskQuery(context_id=context_id, since=since_time)
|
|
735
749
|
|
|
736
750
|
recent_result = await provider.find_tasks(time_query)
|
|
737
751
|
assert recent_result.data is not None
|
|
@@ -743,7 +757,7 @@ class TestTaskQueryAndPagination(TaskLifecycleTestBase):
|
|
|
743
757
|
# All tasks should be newer than since_time
|
|
744
758
|
for task in recent_tasks:
|
|
745
759
|
if task.status.timestamp:
|
|
746
|
-
task_time = datetime.fromisoformat(task.status.timestamp.replace(
|
|
760
|
+
task_time = datetime.fromisoformat(task.status.timestamp.replace("Z", "+00:00"))
|
|
747
761
|
assert task_time >= since_time, f"Task {task.id} is older than since_time"
|
|
748
762
|
|
|
749
763
|
|
|
@@ -768,9 +782,9 @@ class TestTaskErrorHandling(TaskLifecycleTestBase):
|
|
|
768
782
|
result = await provider.update_task(nonexistent_task)
|
|
769
783
|
|
|
770
784
|
# Should fail with appropriate error
|
|
771
|
-
if hasattr(result,
|
|
785
|
+
if hasattr(result, "data"):
|
|
772
786
|
assert result.data is None
|
|
773
|
-
elif hasattr(result,
|
|
787
|
+
elif hasattr(result, "error"):
|
|
774
788
|
assert result.error is not None
|
|
775
789
|
else:
|
|
776
790
|
assert False, "Result should have either data or error"
|
|
@@ -794,7 +808,7 @@ class TestTaskErrorHandling(TaskLifecycleTestBase):
|
|
|
794
808
|
id="", # Invalid empty ID
|
|
795
809
|
contextId="invalid_ctx",
|
|
796
810
|
kind="task",
|
|
797
|
-
status=A2ATaskStatus(state=TaskState.SUBMITTED)
|
|
811
|
+
status=A2ATaskStatus(state=TaskState.SUBMITTED),
|
|
798
812
|
)
|
|
799
813
|
|
|
800
814
|
result = await provider.store_task(invalid_task)
|
|
@@ -845,11 +859,15 @@ class TestProviderHealthAndCleanup(TaskLifecycleTestBase):
|
|
|
845
859
|
|
|
846
860
|
completed_old = self.create_completed_task_update(working_old)
|
|
847
861
|
# Set old timestamp
|
|
848
|
-
completed_old = completed_old.model_copy(
|
|
849
|
-
|
|
850
|
-
"
|
|
851
|
-
|
|
852
|
-
|
|
862
|
+
completed_old = completed_old.model_copy(
|
|
863
|
+
update={
|
|
864
|
+
"status": completed_old.status.model_copy(
|
|
865
|
+
update={
|
|
866
|
+
"timestamp": (datetime.now(timezone.utc) - timedelta(days=8)).isoformat()
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
}
|
|
870
|
+
)
|
|
853
871
|
await provider.update_task(completed_old)
|
|
854
872
|
|
|
855
873
|
# Run cleanup
|