codetether 1.2.2__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.
Files changed (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. ui/monitor.js +2662 -0
@@ -0,0 +1,926 @@
1
+ """
2
+ CodeTether A2A Executor - Bridges official A2A SDK to our task queue system.
3
+
4
+ This module implements the A2A SDK's AgentExecutor interface to bridge standard
5
+ A2A protocol requests to CodeTether's internal task queue and worker system.
6
+
7
+ The executor allows CodeTether to:
8
+ 1. Receive requests via the standard A2A protocol (JSON-RPC over HTTP/SSE)
9
+ 2. Route them through our existing task queue with leasing and concurrency control
10
+ 3. Distribute work to workers via our SSE push system
11
+ 4. Stream results back through the A2A protocol's event system
12
+
13
+ Note: This module uses conditional imports to support both:
14
+ - Full A2A SDK installation (a2a-sdk package)
15
+ - Standalone operation with Protocol-based type stubs
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import logging
22
+ import uuid
23
+ from abc import ABC, abstractmethod
24
+ from dataclasses import dataclass, field
25
+ from datetime import datetime, timezone
26
+ from enum import Enum
27
+ from typing import (
28
+ TYPE_CHECKING,
29
+ Any,
30
+ Callable,
31
+ Dict,
32
+ List,
33
+ Optional,
34
+ Protocol,
35
+ Union,
36
+ runtime_checkable,
37
+ )
38
+
39
+ # Try to import from the official A2A SDK, fall back to local stubs
40
+ try:
41
+ from a2a.server.agent_execution import AgentExecutor, RequestContext
42
+ from a2a.server.events import EventQueue
43
+ from a2a.types import (
44
+ Artifact,
45
+ DataPart,
46
+ FilePart,
47
+ InternalError,
48
+ InvalidParamsError,
49
+ Message,
50
+ Part,
51
+ Task,
52
+ TaskArtifactUpdateEvent,
53
+ TaskState,
54
+ TaskStatus,
55
+ TaskStatusUpdateEvent,
56
+ TextPart,
57
+ UnsupportedOperationError,
58
+ )
59
+
60
+ A2A_SDK_AVAILABLE = True
61
+ except ImportError:
62
+ # A2A SDK not installed - define Protocol-based stubs for type checking
63
+ # These allow the code to be imported and type-checked even without the SDK
64
+ A2A_SDK_AVAILABLE = False
65
+
66
+ class TaskStatus(str, Enum):
67
+ """A2A Task status enum (stub)."""
68
+
69
+ submitted = 'submitted'
70
+ working = 'working'
71
+ input_required = 'input-required'
72
+ completed = 'completed'
73
+ failed = 'failed'
74
+ canceled = 'canceled'
75
+
76
+ @dataclass
77
+ class TaskState:
78
+ """A2A Task state (stub)."""
79
+
80
+ state: TaskStatus
81
+ message: Optional[str] = None
82
+ timestamp: Optional[str] = None
83
+
84
+ @dataclass
85
+ class TextPart:
86
+ """A2A Text part (stub)."""
87
+
88
+ text: str
89
+ type: str = 'text'
90
+
91
+ @dataclass
92
+ class FilePart:
93
+ """A2A File part (stub)."""
94
+
95
+ file: Dict[str, Any]
96
+ type: str = 'file'
97
+
98
+ @dataclass
99
+ class DataPart:
100
+ """A2A Data part (stub)."""
101
+
102
+ data: Any
103
+ mimeType: str = 'application/json'
104
+ type: str = 'data'
105
+
106
+ # Union type for all part types
107
+ Part = Union[TextPart, FilePart, DataPart]
108
+
109
+ @dataclass
110
+ class Message:
111
+ """A2A Message (stub)."""
112
+
113
+ role: str
114
+ parts: List[Part]
115
+ metadata: Optional[Dict[str, Any]] = None
116
+
117
+ @dataclass
118
+ class Task:
119
+ """A2A Task (stub)."""
120
+
121
+ id: str
122
+ status: TaskStatus
123
+ artifacts: Optional[List[Any]] = None
124
+ history: Optional[List[Any]] = None
125
+ metadata: Optional[Dict[str, Any]] = None
126
+
127
+ @dataclass
128
+ class Artifact:
129
+ """A2A Artifact (stub)."""
130
+
131
+ artifactId: str
132
+ name: str
133
+ parts: List[Part]
134
+ metadata: Optional[Dict[str, Any]] = None
135
+
136
+ @dataclass
137
+ class TaskStatusUpdateEvent:
138
+ """A2A Task status update event (stub)."""
139
+
140
+ taskId: str
141
+ status: TaskState
142
+ message: Optional[Message] = None
143
+ final: bool = False
144
+
145
+ @dataclass
146
+ class TaskArtifactUpdateEvent:
147
+ """A2A Task artifact update event (stub)."""
148
+
149
+ taskId: str
150
+ artifact: Artifact
151
+
152
+ @runtime_checkable
153
+ class RequestContext(Protocol):
154
+ """Protocol for A2A request context."""
155
+
156
+ @property
157
+ def task_id(self) -> str:
158
+ """The task ID."""
159
+ ...
160
+
161
+ @property
162
+ def message(self) -> Message:
163
+ """The request message."""
164
+ ...
165
+
166
+ @property
167
+ def metadata(self) -> Optional[Dict[str, Any]]:
168
+ """Optional metadata."""
169
+ ...
170
+
171
+ @runtime_checkable
172
+ class EventQueue(Protocol):
173
+ """Protocol for A2A event queue."""
174
+
175
+ async def enqueue_event(self, event: Any) -> None:
176
+ """Enqueue an event to be sent to the client."""
177
+ ...
178
+
179
+ class AgentExecutor(ABC):
180
+ """Abstract base class for A2A agent executors (stub)."""
181
+
182
+ @abstractmethod
183
+ async def execute(
184
+ self, context: RequestContext, event_queue: EventQueue
185
+ ) -> None:
186
+ """Execute agent logic."""
187
+ ...
188
+
189
+ @abstractmethod
190
+ async def cancel(
191
+ self, context: RequestContext, event_queue: EventQueue
192
+ ) -> None:
193
+ """Cancel a running task."""
194
+ ...
195
+
196
+ class InvalidParamsError(Exception):
197
+ """A2A Invalid params error (stub)."""
198
+
199
+ def __init__(self, message: str, data: Any = None):
200
+ self.message = message
201
+ self.data = data
202
+ super().__init__(message)
203
+
204
+ class InternalError(Exception):
205
+ """A2A Internal error (stub)."""
206
+
207
+ def __init__(self, message: str, data: Any = None):
208
+ self.message = message
209
+ self.data = data
210
+ super().__init__(message)
211
+
212
+ class UnsupportedOperationError(Exception):
213
+ """A2A Unsupported operation error (stub)."""
214
+
215
+ def __init__(self, message: str, data: Any = None):
216
+ self.message = message
217
+ self.data = data
218
+ super().__init__(message)
219
+
220
+
221
+ # Internal imports
222
+ from .task_queue import TaskQueue, TaskRun, TaskRunStatus, TaskLimitExceeded
223
+ from .models import TaskStatus as InternalTaskStatus
224
+
225
+ logger = logging.getLogger(__name__)
226
+
227
+ # Polling configuration
228
+ DEFAULT_POLL_INTERVAL = 0.5 # seconds
229
+ MAX_POLL_DURATION = 300 # 5 minutes max wait
230
+
231
+
232
+ class CodetetherExecutionError(Exception):
233
+ """Base exception for CodeTether executor errors."""
234
+
235
+ def __init__(self, message: str, code: int = -32000, data: Any = None):
236
+ self.message = message
237
+ self.code = code
238
+ self.data = data
239
+ super().__init__(message)
240
+
241
+
242
+ class CodetetherExecutor(AgentExecutor):
243
+ """
244
+ Bridges A2A protocol requests to CodeTether's task queue and worker system.
245
+
246
+ This allows us to:
247
+ 1. Receive requests via standard A2A protocol
248
+ 2. Route them through our existing task queue with leasing
249
+ 3. Distribute to workers via our SSE push system
250
+ 4. Return results back through A2A streaming
251
+
252
+ The executor converts between A2A's Message/Task types and our internal
253
+ TaskRun system, maintaining full compatibility with both protocols.
254
+ """
255
+
256
+ def __init__(
257
+ self,
258
+ task_queue: TaskQueue,
259
+ worker_manager: Optional[Any] = None,
260
+ database: Optional[Any] = None,
261
+ default_user_id: Optional[str] = None,
262
+ default_priority: int = 0,
263
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
264
+ max_poll_duration: float = MAX_POLL_DURATION,
265
+ ):
266
+ """
267
+ Initialize the CodeTether executor.
268
+
269
+ Args:
270
+ task_queue: The TaskQueue instance for enqueuing and tracking tasks
271
+ worker_manager: Optional worker manager for direct worker communication
272
+ database: Optional database connection for task persistence
273
+ default_user_id: Default user ID for tasks without explicit user context
274
+ default_priority: Default priority for new tasks
275
+ poll_interval: How often to poll for task updates (seconds)
276
+ max_poll_duration: Maximum time to wait for task completion (seconds)
277
+ """
278
+ self.task_queue = task_queue
279
+ self.worker_manager = worker_manager
280
+ self.database = database
281
+ self.default_user_id = default_user_id
282
+ self.default_priority = default_priority
283
+ self.poll_interval = poll_interval
284
+ self.max_poll_duration = max_poll_duration
285
+
286
+ async def execute(
287
+ self, context: RequestContext, event_queue: EventQueue
288
+ ) -> None:
289
+ """
290
+ Execute agent logic by routing to our task queue.
291
+
292
+ This method:
293
+ 1. Extracts the message content from the A2A request
294
+ 2. Creates an internal task in our PostgreSQL database
295
+ 3. Enqueues a task_run for worker processing
296
+ 4. Polls for results and streams them back via event_queue
297
+
298
+ Args:
299
+ context: The A2A request context containing message and task info
300
+ event_queue: Queue for sending events back to the client
301
+ """
302
+ task_id = context.task_id
303
+ message = context.message
304
+
305
+ logger.info(f'Executing A2A request for task {task_id}')
306
+
307
+ try:
308
+ # Extract text content from the A2A message
309
+ prompt = self._extract_text_from_message(message)
310
+ if not prompt:
311
+ raise InvalidParamsError(
312
+ message='Message must contain at least one text part'
313
+ )
314
+
315
+ # Extract metadata for routing
316
+ metadata = self._extract_metadata(context)
317
+ user_id = metadata.get('user_id', self.default_user_id)
318
+ priority = metadata.get('priority', self.default_priority)
319
+ target_agent = metadata.get('target_agent_name')
320
+ required_capabilities = metadata.get('required_capabilities')
321
+
322
+ # Create the internal task record
323
+ internal_task_id = await self._create_internal_task(
324
+ task_id=task_id,
325
+ prompt=prompt,
326
+ user_id=user_id,
327
+ metadata=metadata,
328
+ )
329
+
330
+ # Enqueue the task run
331
+ task_run = await self._enqueue_task_run(
332
+ task_id=internal_task_id,
333
+ user_id=user_id,
334
+ priority=priority,
335
+ target_agent_name=target_agent,
336
+ required_capabilities=required_capabilities,
337
+ )
338
+
339
+ logger.info(
340
+ f'Created task_run {task_run.id} for A2A task {task_id}'
341
+ )
342
+
343
+ # Send initial working status
344
+ await self._send_status_update(
345
+ event_queue=event_queue,
346
+ task_id=task_id,
347
+ status=TaskStatus.working,
348
+ message='Task queued for processing',
349
+ )
350
+
351
+ # Poll for results and stream back
352
+ await self._poll_and_stream_results(
353
+ event_queue=event_queue,
354
+ task_id=task_id,
355
+ task_run_id=task_run.id,
356
+ )
357
+
358
+ except TaskLimitExceeded as e:
359
+ logger.warning(f'Task limit exceeded for task {task_id}: {e}')
360
+ await self._send_error(
361
+ event_queue=event_queue,
362
+ task_id=task_id,
363
+ error_message=str(e),
364
+ error_code=-32002,
365
+ error_data=e.to_dict(),
366
+ )
367
+
368
+ except InvalidParamsError:
369
+ raise # Re-raise A2A SDK errors as-is
370
+
371
+ except Exception as e:
372
+ logger.exception(f'Error executing task {task_id}: {e}')
373
+ await self._send_error(
374
+ event_queue=event_queue,
375
+ task_id=task_id,
376
+ error_message=f'Internal execution error: {str(e)}',
377
+ error_code=-32000,
378
+ )
379
+
380
+ async def cancel(
381
+ self, context: RequestContext, event_queue: EventQueue
382
+ ) -> None:
383
+ """
384
+ Cancel a running task.
385
+
386
+ This method:
387
+ 1. Finds the task_run associated with the A2A task
388
+ 2. Attempts to cancel it in our queue
389
+ 3. Sends cancellation confirmation via event_queue
390
+
391
+ Args:
392
+ context: The A2A request context containing task info
393
+ event_queue: Queue for sending events back to the client
394
+ """
395
+ task_id = context.task_id
396
+
397
+ logger.info(f'Cancelling A2A task {task_id}')
398
+
399
+ try:
400
+ # Find the task_run for this A2A task
401
+ task_run = await self._find_task_run_by_external_id(task_id)
402
+
403
+ if not task_run:
404
+ raise InvalidParamsError(
405
+ message=f'No task found with ID {task_id}'
406
+ )
407
+
408
+ # Check if task is in a cancellable state
409
+ if task_run.status in (
410
+ TaskRunStatus.COMPLETED,
411
+ TaskRunStatus.FAILED,
412
+ TaskRunStatus.CANCELLED,
413
+ ):
414
+ logger.info(
415
+ f'Task {task_id} already in terminal state: {task_run.status}'
416
+ )
417
+ await self._send_status_update(
418
+ event_queue=event_queue,
419
+ task_id=task_id,
420
+ status=self._map_internal_to_a2a_status(task_run.status),
421
+ message=f'Task already {task_run.status.value}',
422
+ is_final=True,
423
+ )
424
+ return
425
+
426
+ # Attempt cancellation
427
+ cancelled = await self.task_queue.cancel_run(task_run.id)
428
+
429
+ if cancelled:
430
+ logger.info(f'Successfully cancelled task {task_id}')
431
+ await self._send_status_update(
432
+ event_queue=event_queue,
433
+ task_id=task_id,
434
+ status=TaskStatus.canceled,
435
+ message='Task cancelled successfully',
436
+ is_final=True,
437
+ )
438
+ else:
439
+ # Task may have transitioned to running state
440
+ logger.warning(
441
+ f'Failed to cancel task {task_id} - may be running'
442
+ )
443
+ await self._send_status_update(
444
+ event_queue=event_queue,
445
+ task_id=task_id,
446
+ status=TaskStatus.working,
447
+ message='Task is currently running and cannot be cancelled',
448
+ )
449
+
450
+ except InvalidParamsError:
451
+ raise
452
+
453
+ except Exception as e:
454
+ logger.exception(f'Error cancelling task {task_id}: {e}')
455
+ raise InternalError(message=f'Failed to cancel task: {str(e)}')
456
+
457
+ # -------------------------------------------------------------------------
458
+ # Helper Methods - Message Extraction
459
+ # -------------------------------------------------------------------------
460
+
461
+ def _extract_text_from_message(self, message: Message) -> str:
462
+ """
463
+ Extract text content from an A2A Message.
464
+
465
+ Concatenates all text parts in the message, preserving order.
466
+
467
+ Args:
468
+ message: The A2A Message to extract text from
469
+
470
+ Returns:
471
+ Concatenated text content from all TextParts
472
+ """
473
+ text_parts = []
474
+
475
+ for part in message.parts:
476
+ if isinstance(part, TextPart):
477
+ text_parts.append(part.text)
478
+ elif hasattr(part, 'text'):
479
+ # Handle dict-like parts that may have text
480
+ text_parts.append(str(part.text))
481
+
482
+ return '\n'.join(text_parts)
483
+
484
+ def _extract_metadata(self, context: RequestContext) -> Dict[str, Any]:
485
+ """
486
+ Extract routing and configuration metadata from the request context.
487
+
488
+ Args:
489
+ context: The A2A RequestContext
490
+
491
+ Returns:
492
+ Dictionary of metadata for task routing and configuration
493
+ """
494
+ metadata: Dict[str, Any] = {}
495
+
496
+ # Extract from context metadata if available
497
+ if hasattr(context, 'metadata') and context.metadata:
498
+ ctx_meta = context.metadata
499
+ if isinstance(ctx_meta, dict):
500
+ metadata.update(ctx_meta)
501
+
502
+ # Extract from message metadata
503
+ if context.message and hasattr(context.message, 'metadata'):
504
+ msg_meta = context.message.metadata
505
+ if isinstance(msg_meta, dict):
506
+ metadata.update(msg_meta)
507
+
508
+ return metadata
509
+
510
+ # -------------------------------------------------------------------------
511
+ # Helper Methods - Task Management
512
+ # -------------------------------------------------------------------------
513
+
514
+ async def _create_internal_task(
515
+ self,
516
+ task_id: str,
517
+ prompt: str,
518
+ user_id: Optional[str],
519
+ metadata: Dict[str, Any],
520
+ ) -> str:
521
+ """
522
+ Create an internal task record in the database.
523
+
524
+ Args:
525
+ task_id: The A2A task ID (used as external reference)
526
+ prompt: The task prompt/description
527
+ user_id: Optional user ID
528
+ metadata: Additional task metadata
529
+
530
+ Returns:
531
+ The internal task ID
532
+ """
533
+ if self.database is None:
534
+ # If no database, use the A2A task_id directly
535
+ return task_id
536
+
537
+ try:
538
+ async with self.database.acquire() as conn:
539
+ # Check if task already exists (idempotency)
540
+ existing = await conn.fetchrow(
541
+ "SELECT id FROM tasks WHERE id = $1 OR metadata->>'a2a_task_id' = $1",
542
+ task_id,
543
+ )
544
+ if existing:
545
+ return existing['id']
546
+
547
+ # Create new task
548
+ internal_id = str(uuid.uuid4())
549
+ task_metadata = {
550
+ **metadata,
551
+ 'a2a_task_id': task_id,
552
+ 'source': 'a2a_executor',
553
+ }
554
+
555
+ await conn.execute(
556
+ """
557
+ INSERT INTO tasks (id, title, description, status, user_id, metadata, created_at, updated_at)
558
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $7)
559
+ """,
560
+ internal_id,
561
+ f'A2A Task: {task_id[:8]}',
562
+ prompt[:500] if len(prompt) > 500 else prompt,
563
+ 'pending',
564
+ user_id,
565
+ task_metadata,
566
+ datetime.now(timezone.utc),
567
+ )
568
+
569
+ return internal_id
570
+
571
+ except Exception as e:
572
+ logger.error(f'Failed to create internal task: {e}')
573
+ # Fall back to using A2A task_id
574
+ return task_id
575
+
576
+ async def _enqueue_task_run(
577
+ self,
578
+ task_id: str,
579
+ user_id: Optional[str],
580
+ priority: int,
581
+ target_agent_name: Optional[str] = None,
582
+ required_capabilities: Optional[List[str]] = None,
583
+ ) -> TaskRun:
584
+ """
585
+ Enqueue a task run for worker processing.
586
+
587
+ Args:
588
+ task_id: The internal task ID
589
+ user_id: Optional user ID for concurrency limiting
590
+ priority: Task priority (higher = more urgent)
591
+ target_agent_name: Optional specific agent to route to
592
+ required_capabilities: Optional required worker capabilities
593
+
594
+ Returns:
595
+ The created TaskRun
596
+ """
597
+ return await self.task_queue.enqueue(
598
+ task_id=task_id,
599
+ user_id=user_id,
600
+ priority=priority,
601
+ target_agent_name=target_agent_name,
602
+ required_capabilities=required_capabilities,
603
+ )
604
+
605
+ async def _find_task_run_by_external_id(
606
+ self, a2a_task_id: str
607
+ ) -> Optional[TaskRun]:
608
+ """
609
+ Find a task_run by its A2A external task ID.
610
+
611
+ Args:
612
+ a2a_task_id: The A2A task ID
613
+
614
+ Returns:
615
+ The TaskRun if found, None otherwise
616
+ """
617
+ # First try direct lookup (if A2A task_id was used as internal ID)
618
+ task_run = await self.task_queue.get_run_by_task(a2a_task_id)
619
+ if task_run:
620
+ return task_run
621
+
622
+ # If we have a database, look up via metadata
623
+ if self.database:
624
+ try:
625
+ async with self.database.acquire() as conn:
626
+ row = await conn.fetchrow(
627
+ """
628
+ SELECT tr.* FROM task_runs tr
629
+ JOIN tasks t ON tr.task_id = t.id
630
+ WHERE t.metadata->>'a2a_task_id' = $1
631
+ ORDER BY tr.created_at DESC
632
+ LIMIT 1
633
+ """,
634
+ a2a_task_id,
635
+ )
636
+ if row:
637
+ return self.task_queue._row_to_task_run(row)
638
+ except Exception as e:
639
+ logger.error(f'Error looking up task by A2A ID: {e}')
640
+
641
+ return None
642
+
643
+ # -------------------------------------------------------------------------
644
+ # Helper Methods - Polling and Streaming
645
+ # -------------------------------------------------------------------------
646
+
647
+ async def _poll_and_stream_results(
648
+ self,
649
+ event_queue: EventQueue,
650
+ task_id: str,
651
+ task_run_id: str,
652
+ ) -> None:
653
+ """
654
+ Poll for task completion and stream results back.
655
+
656
+ This method polls the task_run status and streams updates
657
+ back to the client via the event_queue until the task
658
+ reaches a terminal state.
659
+
660
+ Args:
661
+ event_queue: Queue for sending events
662
+ task_id: The A2A task ID
663
+ task_run_id: The internal task_run ID
664
+ """
665
+ start_time = asyncio.get_event_loop().time()
666
+ last_status = None
667
+
668
+ while True:
669
+ # Check timeout
670
+ elapsed = asyncio.get_event_loop().time() - start_time
671
+ if elapsed > self.max_poll_duration:
672
+ logger.warning(f'Task {task_id} timed out after {elapsed:.1f}s')
673
+ await self._send_status_update(
674
+ event_queue=event_queue,
675
+ task_id=task_id,
676
+ status=TaskStatus.failed,
677
+ message=f'Task timed out after {self.max_poll_duration}s',
678
+ is_final=True,
679
+ )
680
+ return
681
+
682
+ # Get current task run status
683
+ task_run = await self.task_queue.get_run(task_run_id)
684
+
685
+ if not task_run:
686
+ logger.error(f'Task run {task_run_id} not found')
687
+ await self._send_error(
688
+ event_queue=event_queue,
689
+ task_id=task_id,
690
+ error_message='Task run not found',
691
+ error_code=-32001,
692
+ )
693
+ return
694
+
695
+ # Send status update if changed
696
+ if task_run.status != last_status:
697
+ last_status = task_run.status
698
+
699
+ a2a_status = self._map_internal_to_a2a_status(task_run.status)
700
+ is_final = task_run.status in (
701
+ TaskRunStatus.COMPLETED,
702
+ TaskRunStatus.FAILED,
703
+ TaskRunStatus.CANCELLED,
704
+ )
705
+
706
+ # Build message based on status
707
+ message = self._build_status_message(task_run)
708
+
709
+ await self._send_status_update(
710
+ event_queue=event_queue,
711
+ task_id=task_id,
712
+ status=a2a_status,
713
+ message=message,
714
+ is_final=is_final,
715
+ )
716
+
717
+ # If we have a result, send it as an artifact
718
+ if is_final and task_run.result_summary:
719
+ await self._send_result_artifact(
720
+ event_queue=event_queue,
721
+ task_id=task_id,
722
+ result=task_run.result_summary,
723
+ full_result=task_run.result_full,
724
+ )
725
+
726
+ if is_final:
727
+ return
728
+
729
+ # Wait before next poll
730
+ await asyncio.sleep(self.poll_interval)
731
+
732
+ def _build_status_message(self, task_run: TaskRun) -> str:
733
+ """Build a human-readable status message for a task run."""
734
+ status_messages = {
735
+ TaskRunStatus.QUEUED: 'Task is queued for processing',
736
+ TaskRunStatus.RUNNING: 'Task is being processed by a worker',
737
+ TaskRunStatus.NEEDS_INPUT: 'Task requires additional input',
738
+ TaskRunStatus.COMPLETED: 'Task completed successfully',
739
+ TaskRunStatus.FAILED: task_run.last_error or 'Task failed',
740
+ TaskRunStatus.CANCELLED: 'Task was cancelled',
741
+ }
742
+ return status_messages.get(
743
+ task_run.status, f'Status: {task_run.status.value}'
744
+ )
745
+
746
+ # -------------------------------------------------------------------------
747
+ # Helper Methods - Event Sending
748
+ # -------------------------------------------------------------------------
749
+
750
+ async def _send_status_update(
751
+ self,
752
+ event_queue: EventQueue,
753
+ task_id: str,
754
+ status: TaskStatus,
755
+ message: str,
756
+ is_final: bool = False,
757
+ ) -> None:
758
+ """
759
+ Send a task status update event.
760
+
761
+ Args:
762
+ event_queue: Queue for sending events
763
+ task_id: The A2A task ID
764
+ status: The new task status
765
+ message: Human-readable status message
766
+ is_final: Whether this is the final update
767
+ """
768
+ # Create the status update event
769
+ event = TaskStatusUpdateEvent(
770
+ taskId=task_id,
771
+ status=TaskState(state=status),
772
+ message=Message(
773
+ role='agent',
774
+ parts=[TextPart(text=message)],
775
+ ),
776
+ final=is_final,
777
+ )
778
+
779
+ await event_queue.enqueue_event(event)
780
+
781
+ async def _send_result_artifact(
782
+ self,
783
+ event_queue: EventQueue,
784
+ task_id: str,
785
+ result: str,
786
+ full_result: Optional[Dict[str, Any]] = None,
787
+ ) -> None:
788
+ """
789
+ Send task result as an artifact.
790
+
791
+ Args:
792
+ event_queue: Queue for sending events
793
+ task_id: The A2A task ID
794
+ result: The result summary text
795
+ full_result: Optional full result data
796
+ """
797
+ # Build artifact parts
798
+ parts: List[Part] = [TextPart(text=result)]
799
+
800
+ # If we have structured data, add it as a DataPart
801
+ if full_result:
802
+ parts.append(
803
+ DataPart(
804
+ data=full_result,
805
+ mimeType='application/json',
806
+ )
807
+ )
808
+
809
+ artifact = Artifact(
810
+ artifactId=str(uuid.uuid4()),
811
+ name='result',
812
+ parts=parts,
813
+ )
814
+
815
+ event = TaskArtifactUpdateEvent(
816
+ taskId=task_id,
817
+ artifact=artifact,
818
+ )
819
+
820
+ await event_queue.enqueue_event(event)
821
+
822
+ async def _send_error(
823
+ self,
824
+ event_queue: EventQueue,
825
+ task_id: str,
826
+ error_message: str,
827
+ error_code: int = -32000,
828
+ error_data: Any = None,
829
+ ) -> None:
830
+ """
831
+ Send an error status update.
832
+
833
+ Args:
834
+ event_queue: Queue for sending events
835
+ task_id: The A2A task ID
836
+ error_message: Error description
837
+ error_code: JSON-RPC error code
838
+ error_data: Optional additional error data
839
+ """
840
+ # Build error message with details
841
+ full_message = error_message
842
+ if error_data:
843
+ full_message = f'{error_message}\nDetails: {error_data}'
844
+
845
+ await self._send_status_update(
846
+ event_queue=event_queue,
847
+ task_id=task_id,
848
+ status=TaskStatus.failed,
849
+ message=full_message,
850
+ is_final=True,
851
+ )
852
+
853
+ # -------------------------------------------------------------------------
854
+ # Helper Methods - Status Mapping
855
+ # -------------------------------------------------------------------------
856
+
857
+ def _map_internal_to_a2a_status(
858
+ self, internal_status: TaskRunStatus
859
+ ) -> TaskStatus:
860
+ """
861
+ Map internal TaskRunStatus to A2A TaskStatus.
862
+
863
+ Args:
864
+ internal_status: Our internal status enum
865
+
866
+ Returns:
867
+ The corresponding A2A TaskStatus
868
+ """
869
+ status_map = {
870
+ TaskRunStatus.QUEUED: TaskStatus.submitted,
871
+ TaskRunStatus.RUNNING: TaskStatus.working,
872
+ TaskRunStatus.NEEDS_INPUT: TaskStatus.input_required,
873
+ TaskRunStatus.COMPLETED: TaskStatus.completed,
874
+ TaskRunStatus.FAILED: TaskStatus.failed,
875
+ TaskRunStatus.CANCELLED: TaskStatus.canceled,
876
+ }
877
+ return status_map.get(internal_status, TaskStatus.working)
878
+
879
+ def _map_a2a_to_internal_status(
880
+ self, a2a_status: TaskStatus
881
+ ) -> TaskRunStatus:
882
+ """
883
+ Map A2A TaskStatus to internal TaskRunStatus.
884
+
885
+ Args:
886
+ a2a_status: The A2A status enum
887
+
888
+ Returns:
889
+ The corresponding internal TaskRunStatus
890
+ """
891
+ status_map = {
892
+ TaskStatus.submitted: TaskRunStatus.QUEUED,
893
+ TaskStatus.working: TaskRunStatus.RUNNING,
894
+ TaskStatus.input_required: TaskRunStatus.NEEDS_INPUT,
895
+ TaskStatus.completed: TaskRunStatus.COMPLETED,
896
+ TaskStatus.failed: TaskRunStatus.FAILED,
897
+ TaskStatus.canceled: TaskRunStatus.CANCELLED,
898
+ }
899
+ return status_map.get(a2a_status, TaskRunStatus.QUEUED)
900
+
901
+
902
+ # Factory function for easy instantiation
903
+ def create_codetether_executor(
904
+ task_queue: TaskQueue,
905
+ worker_manager: Optional[Any] = None,
906
+ database: Optional[Any] = None,
907
+ **kwargs,
908
+ ) -> CodetetherExecutor:
909
+ """
910
+ Factory function to create a CodetetherExecutor instance.
911
+
912
+ Args:
913
+ task_queue: The TaskQueue instance
914
+ worker_manager: Optional worker manager
915
+ database: Optional database connection pool
916
+ **kwargs: Additional configuration options
917
+
918
+ Returns:
919
+ Configured CodetetherExecutor instance
920
+ """
921
+ return CodetetherExecutor(
922
+ task_queue=task_queue,
923
+ worker_manager=worker_manager,
924
+ database=database,
925
+ **kwargs,
926
+ )