Task 007 implement agent job queue and fake runner path
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
from urllib import request
|
||||
|
||||
|
||||
class BackendJobClient(Protocol):
|
||||
def claim_job(self) -> dict[str, Any] | None:
|
||||
pass
|
||||
|
||||
def complete_job(self, job_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
pass
|
||||
|
||||
|
||||
class HttpBackendJobClient:
|
||||
def __init__(self, backend_url: str) -> None:
|
||||
self.backend_url = backend_url.rstrip("/")
|
||||
|
||||
def claim_job(self) -> dict[str, Any] | None:
|
||||
response = self._post_json("/internal/agent-jobs/claim", None)
|
||||
if response is None:
|
||||
return None
|
||||
return response["job"]
|
||||
|
||||
def complete_job(self, job_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
response = self._post_json(f"/internal/agent-jobs/{job_id}/complete", payload)
|
||||
if response is None:
|
||||
raise RuntimeError("Backend returned empty completion response")
|
||||
return response["job"]
|
||||
|
||||
def _post_json(self, path: str, payload: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||
http_request = request.Request(
|
||||
f"{self.backend_url}{path}",
|
||||
data=data,
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
with request.urlopen(http_request, timeout=5) as response:
|
||||
if response.status == 204:
|
||||
return None
|
||||
body = response.read().decode("utf-8")
|
||||
except Exception as error:
|
||||
raise RuntimeError(f"Backend request failed: {path}") from error
|
||||
|
||||
if not body:
|
||||
return None
|
||||
return json.loads(body)
|
||||
|
||||
|
||||
def run_fake_runner_once(
|
||||
backend_client: BackendJobClient,
|
||||
*,
|
||||
workspace_root: Path,
|
||||
) -> dict[str, Any] | None:
|
||||
job = backend_client.claim_job()
|
||||
if job is None:
|
||||
return None
|
||||
|
||||
started = time.monotonic()
|
||||
workspace_path = materialize_fake_workspace(job, workspace_root=workspace_root)
|
||||
stdout = "fake Codex runner completed\n"
|
||||
stderr = ""
|
||||
output: dict[str, Any]
|
||||
if job["agent_profile"] == "fake-invalid-schema":
|
||||
output = {"status": "NOT_A_STATUS"}
|
||||
else:
|
||||
output = {
|
||||
"status": "SUCCEEDED",
|
||||
"output_files": [{"path": "outputs/result.json"}],
|
||||
"payload": {"job_id": job["id"], "mode": "fake"},
|
||||
}
|
||||
|
||||
duration_ms = int((time.monotonic() - started) * 1000)
|
||||
return backend_client.complete_job(
|
||||
job["id"],
|
||||
{
|
||||
"workspace_path": str(workspace_path),
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"exit_code": 0,
|
||||
"duration_ms": duration_ms,
|
||||
"output": output,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def materialize_fake_workspace(
|
||||
job: dict[str, Any],
|
||||
*,
|
||||
workspace_root: Path,
|
||||
) -> Path:
|
||||
workspace_path = workspace_root / job["id"]
|
||||
if workspace_path.exists():
|
||||
shutil.rmtree(workspace_path)
|
||||
(workspace_path / "inputs").mkdir(parents=True)
|
||||
(workspace_path / "logs").mkdir()
|
||||
(workspace_path / "outputs").mkdir()
|
||||
|
||||
(workspace_path / "inputs" / "job.json").write_text(
|
||||
json.dumps(job, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(workspace_path / "logs" / "stdout.log").write_text(
|
||||
"fake Codex runner completed\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(workspace_path / "logs" / "stderr.log").write_text("", encoding="utf-8")
|
||||
(workspace_path / "outputs" / "result.json").write_text(
|
||||
json.dumps({"status": "ok", "job_id": job["id"]}, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return workspace_path
|
||||
@@ -2,9 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from src.application.fake_runner import get_backend_url, get_workspace_root, run_fake_runner_loop
|
||||
|
||||
|
||||
class HealthHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
@@ -24,6 +27,21 @@ class HealthHandler(BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
def run() -> None:
|
||||
mode = os.environ.get("RUNNER_MODE", "fake").lower()
|
||||
if mode == "fake":
|
||||
thread = threading.Thread(
|
||||
target=run_fake_runner_loop,
|
||||
kwargs={
|
||||
"backend_url": get_backend_url(),
|
||||
"workspace_root": get_workspace_root(),
|
||||
"poll_interval_seconds": float(
|
||||
os.environ.get("RUNNER_POLL_INTERVAL_SECONDS", "1.0")
|
||||
),
|
||||
},
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
|
||||
port = int(os.environ.get("PORT", "8010"))
|
||||
server = ThreadingHTTPServer(("0.0.0.0", port), HealthHandler)
|
||||
server.serve_forever()
|
||||
|
||||
Reference in New Issue
Block a user