"""Temporal workflow definitions for activity-core. Two workflows are registered here: - RunActivityWorkflow → orchestrator-tq - TaskExecutorWorkflow → task-execution-tq Workflow IDs follow the conventions in docs/conventions.md: RunActivityWorkflow: activity-{activity_id}:{trigger_key} TaskExecutorWorkflow: task-{run_id}:{task_type}:{index} """ from __future__ import annotations import uuid from datetime import timedelta from temporalio import workflow from temporalio.common import RetryPolicy with workflow.unsafe.imports_passed_through(): from activity_core.activities import ( load_activity_definition, log_run, resolve_context, ) from activity_core.template_engine import evaluate_templates _RETRY_POLICY = RetryPolicy( initial_interval=timedelta(seconds=1), backoff_coefficient=2.0, maximum_interval=timedelta(minutes=5), maximum_attempts=10, ) _ACTIVITY_TIMEOUT = timedelta(minutes=5) _TASK_QUEUE = "task-execution-tq" @workflow.defn class RunActivityWorkflow: """Durable orchestration workflow. Sequence: 1. load_activity_definition(activity_id) → defn dict 2. resolve_context(defn.context_sources) → context snapshot 3. evaluate_templates(templates, context) → task specs (pure, no activity) 4. log_run(...) → run_id 5. start_child_workflow per task spec (fire-and-forget, detached) """ @workflow.run async def run( self, activity_id: str, trigger_key: str, scheduled_for: str | None = None, ) -> dict: """ Args: activity_id: UUID of the ActivityDefinition row. trigger_key: ISO-8601 datetime (cron) or event_id (event trigger). Used as the idempotency key component. scheduled_for: ISO-8601 string of the nominal scheduled time (cron only). Returns: {"run_id": str, "tasks_spawned": int} """ # ── 1. Load definition ──────────────────────────────────────────────── defn: dict = await workflow.execute_activity( load_activity_definition, activity_id, start_to_close_timeout=_ACTIVITY_TIMEOUT, retry_policy=_RETRY_POLICY, ) # ── 2. Resolve context ──────────────────────────────────────────────── context_snapshot: dict = await workflow.execute_activity( resolve_context, defn["context_sources"], start_to_close_timeout=_ACTIVITY_TIMEOUT, retry_policy=_RETRY_POLICY, ) # ── 3. Evaluate templates (pure — no activity) ──────────────────────── task_specs: list[dict] = evaluate_templates( defn["task_templates"], context_snapshot ) # ── 4. Log the run ──────────────────────────────────────────────────── # run_id is derived deterministically so log_run retries are idempotent. run_id = str( uuid.uuid5(uuid.NAMESPACE_URL, f"{activity_id}:{trigger_key}") ) await workflow.execute_activity( log_run, { "run_id": run_id, "activity_id": activity_id, "scheduled_for": scheduled_for, "context_snapshot": context_snapshot, "tasks_spawned": len(task_specs), "version_used": defn["version"], }, start_to_close_timeout=_ACTIVITY_TIMEOUT, retry_policy=_RETRY_POLICY, ) # ── 5. Spawn task executor children (fire-and-forget) ───────────────── for index, spec in enumerate(task_specs): child_id = f"task-{run_id}:{spec['task_type']}:{index}" await workflow.start_child_workflow( TaskExecutorWorkflow, args=[run_id, spec["task_type"], spec["params"]], id=child_id, task_queue=_TASK_QUEUE, parent_close_policy=workflow.ParentClosePolicy.ABANDON, ) return {"run_id": run_id, "tasks_spawned": len(task_specs)} @workflow.defn class TaskExecutorWorkflow: """Child workflow that executes one concrete task instance. Stub implementation — T19. """ @workflow.run async def run(self, run_id: str, task_type: str, params: dict) -> dict: raise NotImplementedError("T19")