Xuehao Xu cfe2bec371
feat(workflows): add multi-Gateway coordination and session-authorized failover (#7138)
* feat(workflows): add leased multi-Gateway coordination and fenced dispatch

* test(workflows): validate deployed failover and canonicalize config revisions

* fix(workflows): share namespace across registration and lease fencing

* fix(workflows): recover interrupted cancellation without admission caps

* fix(workflows): retain execution lease through child cleanup

* docs: trim guidance padding to fit merged instruction budget

* fix(workflows): release rejected admission slots and normalize gateway routing

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-23 23:31:43 +08:00

128 lines
5.6 KiB
Python

"""Version-only file capture, review and consumption. Never reopen source paths."""
import asyncio
import base64
import re
from pathlib import PurePosixPath
class Artifacts:
async def capture(self, entry, message):
from .service import text
payload = message.get("payload")
if not isinstance(payload, dict) or set(payload) != {
"stepId",
"sourceStepId",
"path",
}:
raise ValueError("captureArtifact requires stepId, sourceStepId and path")
record, owner = entry["record"], entry["owner"]
step_id = text(payload["stepId"], 80)
if entry.get("recover_frame"):
info = next((a for a in record["artifacts"] if a["step_id"] == step_id), None)
if info:
await asyncio.to_thread(self.store.artifact, owner, record["id"], info["version"])
return {
"id": message["id"],
"value": {**info, "artifactVersion": info["version"]},
}
retained = await asyncio.to_thread(self.store.artifact_by_step, owner, record["id"], step_id)
if retained:
source = next(s for s in record["steps"] if s["step_id"] == payload["sourceStepId"])
info = retained
info.update(
source_step_id=source["step_id"],
source_run_id=source["run_id"],
source_thread_id=source["thread_id"],
)
record["artifacts"].append(info)
await self.save(owner, record)
return {
"id": message["id"],
"value": {**info, "artifactVersion": info["version"]},
}
if not re.fullmatch(r"[A-Za-z0-9_-]{1,80}", step_id) or any(s["step_id"] == step_id for s in record["steps"] + record["checkpoints"] + record["artifacts"]):
raise ValueError("Invalid or reused artifact step ID")
source = next(
(s for s in record["steps"] if s["step_id"] == payload["sourceStepId"] and s["status"] == "completed"),
None,
)
if source is None:
raise ValueError("Artifact source must be a completed Agent step")
path = text(payload["path"], 1000)
name = text(PurePosixPath(path).name, 160)
async with asyncio.timeout(10):
content = await entry["runs"].read_artifact(thread_id=source["thread_id"], path=path)
info = await asyncio.to_thread(self.store.capture_artifact, owner, record["id"], step_id, name, content, **({"execution_record": record} if self.clustered else {}))
info.update(
source_step_id=source["step_id"],
source_run_id=source["run_id"],
source_thread_id=source["thread_id"],
)
record["artifacts"].append(info)
await self.save(owner, record)
return {
"id": message["id"],
"value": {**info, "artifactVersion": info["version"]},
}
@staticmethod
def approved_artifact(record, version):
return any(c.get("artifact_kind") == "file" and c.get("artifact_version") == version and c["status"] == "approved" for c in record.get("checkpoints", []))
async def artifact_chunk(self, owner, payload, *, require_approval):
from .service import text
if set(payload) - {"run_id", "version", "offset"}:
raise ValueError("Unexpected artifact read fields")
run_id, version = (
text(payload.get("run_id"), 64),
text(payload.get("version"), 64),
)
record = await asyncio.to_thread(self.store.get, owner, run_id)
if require_approval and not self.approved_artifact(record, version):
raise ValueError("File version has not been approved")
offset = payload.get("offset", 0)
if type(offset) is not int or offset < 0:
raise ValueError("Invalid artifact offset")
info, content = await asyncio.to_thread(self.store.artifact, owner, run_id, version)
if offset > len(content):
raise ValueError("Offset exceeds artifact size")
end = min(offset + 16384, len(content))
return {
**info,
"offset": offset,
"next_offset": end if end < len(content) else None,
"content_base64": base64.b64encode(content[offset:end]).decode("ascii"),
}
async def get_artifact(self, payload, context):
"""Browser review reads the same stored bytes as downstream consumers."""
from deerflow_extension_api.plugins import ToolContext
if isinstance(context, ToolContext):
raise ValueError("Use the approved-artifact consumer tool")
return await self.artifact_chunk(self.owner(context), payload, require_approval=False)
async def read_artifact(self, payload, context):
return await self.artifact_chunk(self.owner(context), payload, require_approval=True)
async def consume(self, entry, message):
payload = message.get("payload")
if not isinstance(payload, dict) or set(payload) - {
"artifactVersion",
"offset",
}:
raise ValueError("readArtifact requires artifactVersion and optional offset")
result = await self.artifact_chunk(
entry["owner"],
{
"run_id": entry["record"]["id"],
"version": payload.get("artifactVersion"),
"offset": payload.get("offset", 0),
},
require_approval=True,
)
return {"id": message["id"], "value": result}