candidate.py 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. """Lifecycle-exclusive candidate version command for application runs."""
  2. from __future__ import annotations
  3. import json
  4. from typing import Any, Literal
  5. from pydantic import BaseModel, ConfigDict, Field
  6. from cyber_agent.application.candidate import CandidatePointer
  7. from cyber_agent.application.candidate_service import CandidateStateError
  8. from cyber_agent.tools import tool
  9. from cyber_agent.tools.models import ToolResult
  10. class CandidateManagementRequest(BaseModel):
  11. model_config = ConfigDict(extra="forbid")
  12. operation: Literal["create", "fork", "merge"]
  13. content: Any
  14. parent_refs: list[CandidatePointer] = Field(default_factory=list, max_length=20)
  15. @tool(
  16. description=(
  17. "Create, fork, or merge one application candidate version. The framework "
  18. "owns identity and lineage; the application repository owns content."
  19. ),
  20. hidden_params=["context"],
  21. groups=["core"],
  22. )
  23. async def manage_candidate(
  24. request: CandidateManagementRequest | dict[str, Any],
  25. context: dict | None = None,
  26. ) -> ToolResult | dict[str, Any]:
  27. if not context:
  28. return {"status": "failed", "error": "context is required"}
  29. if context.get("side_branch"):
  30. return {"status": "failed", "error": "Side branches cannot manage candidates"}
  31. try:
  32. request = CandidateManagementRequest.model_validate(request)
  33. except ValueError as exc:
  34. return {"status": "failed", "error": f"Invalid candidate request: {exc}"}
  35. service = context.get("candidate_service")
  36. trace_id = context.get("trace_id")
  37. if service is None or not trace_id:
  38. return {
  39. "status": "failed",
  40. "error": "CandidateService is unavailable for this application",
  41. }
  42. try:
  43. candidate = await service.manage(
  44. trace_id,
  45. operation=request.operation,
  46. content=request.content,
  47. parent_refs=request.parent_refs,
  48. effective_at_sequence=int(context.get("sequence", 0) or 0),
  49. )
  50. except (CandidateStateError, ValueError) as exc:
  51. return {"status": "failed", "error": str(exc)}
  52. payload = candidate.model_dump(mode="json")
  53. return ToolResult(
  54. title="",
  55. output=json.dumps({
  56. "status": "completed",
  57. "candidate_ref": payload,
  58. }, ensure_ascii=False),
  59. artifact_refs=[candidate.artifact_ref.model_dump(mode="json")],
  60. )