Files
mjalmoudy94andClaude Sonnet 5.5 fef5a9deab feat(integration): wire v2 workstreams together (engine policy/budget, sandbox, tenant checks, order route, docs)
Engine honours org policy and monthly budget, legacy runner skips engine tasks,
sandbox session provisioned on demand, project fallback for threads, reviewer
fallback, tenant checks on project ids, memory reflection respects grants,
expiry worker, MCP pipeline_status, login redirect, sharing settings, e2e smoke.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:57:56 +03:30

158 lines
6.7 KiB
Python

"""AI runtime worker — drives queued tasks through the run engine.
The HTTP API can start a run inline (that is what the "اجرا" button does), but
a platform that only ever runs work on the request thread has two problems: a
slow model call holds a connection open, and a task created by anything other
than a button press never runs at all. This worker is the second driver.
It deliberately owns **no** orchestration logic. It finds runnable tasks and
hands each one to ``WorkforceService.start_run`` — the same call the router
makes. Model routing, usage accounting, the timeline and the artifact are all
the service's business.
"""
from __future__ import annotations
from app.application.container import build_workforce_service
from app.application.container_engine import build_scheduler
from app.application.ports.realtime import emit_worker_status
from app.core.errors import AppError
from app.domain.audit.models import ErrorSeverity, ErrorSource
from app.domain.workforce.models import TaskStatus
from app.infrastructure.error_log import record_error
from app.infrastructure.repositories.workforce import TaskRepository
from app.workers.base import WorkerOutcome, worker_session
class AiRuntimeWorker:
"""Drains the queue of tasks that nobody has run yet.
The worker drives **two** engines in one pass, oldest first:
* the legacy run engine — runnable tasks handed to
``WorkforceService.start_run``;
* the workforce engine (Phase 4) — one ``Scheduler.tick()``, which moves
every ready task one stage forward (a turn, a gate, or a platform merge).
Both run on the same session, so a run started here and a stage advanced
here commit together. ``run_once`` (the CLI default) makes one pass — the
cron shape; ``--loop`` keeps ticking (set the interval to
``ENGINE_TICK_SECONDS``).
"""
name = "ai-runtime"
async def drain(self, *, limit: int = 10) -> WorkerOutcome:
"""Run up to ``limit`` queued tasks, then one engine tick.
A task that fails is recorded and the pass continues: one poisoned task
must not block the queue behind it.
"""
outcome = WorkerOutcome(name=self.name)
async with worker_session() as session:
await self._drain_runs(session, outcome, limit=limit)
await self._tick_engine(session, outcome)
return outcome
async def _drain_runs(self, session, outcome: WorkerOutcome, *, limit: int) -> None:
tasks = TaskRepository(session)
workforce = build_workforce_service(session)
for task in await tasks.list_runnable(limit=limit):
# Re-read through the service so the tenant check and the
# "already closed" guard both apply. The worker gets no
# privileged bypass.
try:
current = await workforce.get_task_or_404(
organization_id=task.organization_id, task_id=task.id
)
except AppError:
outcome.skipped += 1
outcome.note(f"task {task.id}: vanished before the run")
continue
if current.status not in (TaskStatus.NEW, TaskStatus.TRIAGED):
outcome.skipped += 1
continue
try:
run = await workforce.start_run(
organization_id=task.organization_id,
task_id=task.id,
user_id=task.created_by_user_id or "",
employee_id=task.assigned_employee_id,
)
except Exception as exc: # noqa: BLE001 - keep draining
outcome.failed += 1
outcome.note(f"task {task.id}: {type(exc).__name__}: {exc}")
# Persist with the tenant attached: a task that fails to run
# is a tenant-visible problem, and the console must be able
# to show that organization what happened.
await record_error(
kind="task_run_failed",
message=str(exc) or type(exc).__name__,
exc=exc,
source=ErrorSource.WORKER,
severity=ErrorSeverity.ERROR,
organization_id=task.organization_id,
user_id=task.created_by_user_id,
metadata={
"worker": self.name,
"task_id": task.id,
"employee_id": task.assigned_employee_id,
},
)
continue
outcome.processed += 1
outcome.note(f"task {task.id} -> run {run.id} ({run.status})")
# This worker is its own process: without a push here, a run
# started by the queue is invisible until someone reloads.
await emit_worker_status(
organization_id=task.organization_id,
worker=self.name,
message=f"کار «{current.title}» در حال اجراست (اجرا {run.id}).",
)
async def _tick_engine(self, session, outcome: WorkerOutcome) -> None:
"""One scheduler tick: move every ready task one step.
A tick failure is recorded against the worker and the pass ends — the
run drain has already committed its own work, and a broken tick must
not look like a clean one.
"""
try:
scheduler = build_scheduler(session)
# A wide read: states that cannot run this pass (automation off, or nobody free
# for their stage) must not fill the window and starve the other organizations.
# Scheduler._select still bounds the real work per organization and per pass.
report = await scheduler.tick(limit=500)
except Exception as exc: # noqa: BLE001 - a worker must survive a bad tick
outcome.failed += 1
outcome.note(f"engine tick: {type(exc).__name__}: {exc}")
await record_error(
kind="engine_tick_failed",
message=str(exc) or type(exc).__name__,
exc=exc,
source=ErrorSource.WORKER,
severity=ErrorSeverity.ERROR,
metadata={"worker": self.name},
)
return
outcome.processed += report.acted + report.merged
outcome.skipped += report.skipped
outcome.failed += report.failed
if report.selected or report.merged or report.paused:
outcome.note(
f"engine tick: selected={report.selected} acted={report.acted} "
f"merged={report.merged} paused={report.paused} failed={report.failed}"
)
for note in report.notes:
outcome.note(note)
__all__ = ["AiRuntimeWorker"]