candidate.py 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  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. command_id = context.get("tool_call_id")
  38. if service is None or not trace_id:
  39. return {
  40. "status": "failed",
  41. "error": "CandidateService is unavailable for this application",
  42. }
  43. if not command_id:
  44. return {
  45. "status": "failed",
  46. "error": "A persisted tool_call_id is required to manage candidates",
  47. }
  48. try:
  49. candidate = await service.manage(
  50. trace_id,
  51. operation=request.operation,
  52. content=request.content,
  53. parent_refs=request.parent_refs,
  54. effective_at_sequence=int(context.get("sequence", 0) or 0),
  55. command_id=str(command_id),
  56. )
  57. except (CandidateStateError, ValueError) as exc:
  58. return {"status": "failed", "error": str(exc)}
  59. payload = candidate.model_dump(mode="json")
  60. return ToolResult(
  61. title="",
  62. output=json.dumps({
  63. "status": "completed",
  64. "candidate_ref": payload,
  65. }, ensure_ascii=False),
  66. artifact_refs=[candidate.artifact_ref.model_dump(mode="json")],
  67. )