feat(tasks): add skill-templated task graph execution

This commit is contained in:
2026-06-23 10:22:58 +08:00
parent 6843d89b2c
commit 53b13e8eac
53 changed files with 4773 additions and 756 deletions

View File

@ -40,6 +40,10 @@ class UserFileSizeError(UserFileError):
"""Raised when a user file upload exceeds configured limits."""
class UserFileStorageError(UserFileError):
"""Raised when the backing user-file storage cannot complete an operation."""
@dataclass(frozen=True, slots=True)
class AgentUserFilePolicy:
task_id: str | None = None
@ -387,26 +391,34 @@ class MinIOUserFileStorage:
async def list_dir(self, path: str) -> list[UserFileEntry]:
prefix = self._object_prefix(path)
objects = self.client.list_objects(self.config.bucket, prefix=prefix, recursive=False)
try:
objects = self.client.list_objects(self.config.bucket, prefix=prefix, recursive=False)
except Exception as exc:
raise _minio_storage_error("list directory", exc) from exc
entries: list[UserFileEntry] = []
for obj in objects:
object_name = str(obj.object_name or "")
user_path = self._user_path(object_name)
if not user_path or user_path == path or user_path.endswith("/.keep"):
continue
trimmed = user_path.rstrip("/")
name = PurePosixPath(trimmed).name
is_dir = bool(getattr(obj, "is_dir", False)) or object_name.endswith("/")
entries.append(
UserFileEntry(
name=name,
path=trimmed,
type="directory" if is_dir else "file",
size=None if is_dir else getattr(obj, "size", None),
content_type=None if is_dir else "application/octet-stream",
modified=obj.last_modified.isoformat() if getattr(obj, "last_modified", None) else None,
try:
for obj in objects:
object_name = str(obj.object_name or "")
user_path = self._user_path(object_name)
if not user_path or user_path == path or user_path.endswith("/.keep"):
continue
trimmed = user_path.rstrip("/")
name = PurePosixPath(trimmed).name
is_dir = bool(getattr(obj, "is_dir", False)) or object_name.endswith("/")
entries.append(
UserFileEntry(
name=name,
path=trimmed,
type="directory" if is_dir else "file",
size=None if is_dir else getattr(obj, "size", None),
content_type=None if is_dir else "application/octet-stream",
modified=obj.last_modified.isoformat() if getattr(obj, "last_modified", None) else None,
)
)
)
except UserFileError:
raise
except Exception as exc:
raise _minio_storage_error("list directory", exc) from exc
return sorted(entries, key=lambda item: (item.type != "directory", item.name.lower()))
async def read_file(self, path: str, *, max_bytes: int | None = None) -> UserFileContent:
@ -421,7 +433,9 @@ class MinIOUserFileStorage:
response.close()
response.release_conn()
except Exception as exc:
raise UserFileNotFoundError("File not found") from exc
if _minio_error_code(exc) in {"NoSuchKey", "NoSuchObject"}:
raise UserFileNotFoundError("File not found") from exc
raise _minio_storage_error("read file", exc) from exc
return UserFileContent(
name=PurePosixPath(path).name,
path=path,
@ -433,13 +447,16 @@ class MinIOUserFileStorage:
async def write_file(self, path: str, content: bytes, *, content_type: str) -> UserFileEntry:
object_name = self._object_name(path)
result = self.client.put_object(
self.config.bucket,
object_name,
BytesIO(content),
length=len(content),
content_type=content_type,
)
try:
self.client.put_object(
self.config.bucket,
object_name,
BytesIO(content),
length=len(content),
content_type=content_type,
)
except Exception as exc:
raise _minio_storage_error("write file", exc) from exc
return UserFileEntry(
name=PurePosixPath(path).name,
path=path,
@ -475,6 +492,8 @@ class MinIOUserFileStorage:
except Exception:
pass
raise
except Exception as exc:
raise _minio_storage_error("write file", exc) from exc
return UserFileEntry(
name=PurePosixPath(path).name,
path=path,
@ -490,23 +509,30 @@ class MinIOUserFileStorage:
try:
self.client.remove_object(self.config.bucket, object_name)
removed = True
except Exception:
pass
except Exception as exc:
if _minio_error_code(exc) != "NoSuchKey":
raise _minio_storage_error("delete path", exc) from exc
prefix = f"{object_name.rstrip('/')}/"
for obj in self.client.list_objects(self.config.bucket, prefix=prefix, recursive=True):
self.client.remove_object(self.config.bucket, str(obj.object_name))
removed = True
try:
for obj in self.client.list_objects(self.config.bucket, prefix=prefix, recursive=True):
self.client.remove_object(self.config.bucket, str(obj.object_name))
removed = True
except Exception as exc:
raise _minio_storage_error("delete path", exc) from exc
return removed
async def mkdir(self, path: str) -> UserFileEntry:
object_name = f"{self._object_name(path).rstrip('/')}/.keep"
self.client.put_object(
self.config.bucket,
object_name,
BytesIO(b""),
length=0,
content_type="application/x-directory",
)
try:
self.client.put_object(
self.config.bucket,
object_name,
BytesIO(b""),
length=0,
content_type="application/x-directory",
)
except Exception as exc:
raise _minio_storage_error("create directory", exc) from exc
return UserFileEntry(
name=PurePosixPath(path).name,
path=path,
@ -600,6 +626,18 @@ def _safe_scope(value: str | None) -> str:
return cleaned or "interactive"
def _minio_error_code(exc: Exception) -> str:
return str(getattr(exc, "code", "") or "")
def _minio_storage_error(operation: str, exc: Exception) -> UserFileStorageError:
code = _minio_error_code(exc)
message = f"User file storage {operation} failed"
if code:
message = f"{message}: {code}"
return UserFileStorageError(message)
class _LimitedReadStream:
def __init__(self, stream: object, *, max_bytes: int | None = None) -> None:
self.stream = stream