Coverage for core/langgraph/_builtins_ticket.py: 100%

53 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-25 03:05 +0000

1"""_builtins_ticket.py — TICKET-profile builtins (lazy-loaded, needs httpx). 

2 

3The TICKET profile (RUN_PROFILE=TICKET) is the Vikunja-driven run shape: 

4checkout from the base branch (git-checkout builtin) → per-run feature 

5branch `ticket/<issue_key>-<slug(title)>` → ticket-publish commits the 

6pipeline output and pushes the branch (only when a commit was created — 

7the branch is born with the first real commit; no empty branch publish 

8and never a forced push) → the PR is ensured idempotently (reuse open → 

9"reused", closed → "reopened", else create → "created"; base = the 

10ticket's base branch). Later steps and continuation runs continue on the 

11SAME branch and PR. A push conflict is a hard error (resolved MVP 

12decision). PR title/body come from the ticket (PR_TITLE / PR_DESCRIPTION). 

13 

14The publish builtin promotes the ensured PR url to the top-level `pr_url` 

15state key — the entrypoint's final status-callback PATCH 

16(UpdateRunRequest.pr_url, TICKET profile) reads it for the exit-0 pr_open 

17lane transition. 

18 

19Decision builtins (ticket-dor / ticket-review) stop the run with 

20outcome-coded TicketOutcome exceptions instead of returning state: 

21 

22 | code | meaning | lane (API-owned mapping) | 

23 |------|----------------------------|--------------------------| 

24 | 10 | DoR inputs insufficient | open_questions | 

25 | 11 | agent: blocked | blocked | 

26 | 20 | review: fixable issues | needs_rework | 

27 | 21 | review: too severe | pr_rejected | 

28 | 22 | review: cannot fix | open_questions | 

29 | 0 | approved → state returned | pr_open | 

30 

31The entrypoint maps TicketOutcome → phase=failed + exit_code + optional 

32`questions` in the final status-callback PATCH (UpdateRunRequest.questions; 

33API posts them as a ticket comment when the lane mapping yields 

34open_questions). The LLM decisions use the pipeline's invoke_agent_fn — 

35GraphBuilder stores it on the BuiltinExecutor (invoke_agent_fn) and 

36register_all binds it to the builtins. 

37""" 

38from __future__ import annotations 

39 

40from typing import Any 

41 

42# Shared ticket HTTP/PM/env helpers live in the non-tool sibling module 

43# tools/_ticket_common.py (single source). Re-exported because existing 

44# importers (tests) resolve them from this module. 

45try: 

46 from tools._ticket_common import ( 

47 _T, _as_questions, _dedupe_skip, _fetch_open_pm_tickets, _hard_failure, 

48 _http_get, _http_patch, _http_post, _json_list_response, 

49 _llm_json_decision, _raise_for_http_status, _repo_full_name, 

50 _require_env, _require_pm_env, _response_json, _typed_response, 

51 _valid_dedupe_tickets, _valid_tickets, TicketOutcome, 

52 ) 

53except ImportError: # pragma: no cover — main.py run (qualified imports) 

54 from langgraph.tools._ticket_common import ( 

55 _T, _as_questions, _dedupe_skip, _fetch_open_pm_tickets, _hard_failure, 

56 _http_get, _http_patch, _http_post, _json_list_response, 

57 _llm_json_decision, _raise_for_http_status, _repo_full_name, 

58 _require_env, _require_pm_env, _response_json, _typed_response, 

59 _valid_dedupe_tickets, _valid_tickets, TicketOutcome, 

60 ) 

61 

62# Publish-only git/PR helpers and the commit-author constants live in the 

63# per-tool module tools/ticket_publish.py (single source). Re-exported because 

64# existing importers (tests) resolve them from this module. 

65try: 

66 from tools.ticket_publish import ( 

67 _ensure_pr, _git, _git_commit_and_push, 

68 ) 

69except ImportError: # pragma: no cover — main.py run (qualified imports) 

70 from langgraph.tools.ticket_publish import ( 

71 _ensure_pr, _git, _git_commit_and_push, 

72 ) 

73 

74# Review-only helpers live in the per-tool module tools/ticket_review.py 

75# (single source). Re-exported because existing importers (tests) resolve 

76# them from this module. 

77try: 

78 from tools.ticket_review import _wait_for_head_review 

79except ImportError: # pragma: no cover — main.py run (qualified imports) 

80 from langgraph.tools.ticket_review import _wait_for_head_review 

81 

82 

83def builtin_ticket_publish(step: dict, state: dict) -> dict: 

84 try: 

85 from tools.ticket_publish import ticket_publish as _exec 

86 except ImportError: # pragma: no cover — main.py run (qualified imports) 

87 from langgraph.tools.ticket_publish import ticket_publish as _exec 

88 return _exec(step, state) 

89 

90 

91def builtin_ticket_dor(step: dict, state: dict, invoke_agent: Any) -> dict: 

92 try: 

93 from tools.ticket_dor import ticket_dor as _exec 

94 except ImportError: # pragma: no cover — main.py run (qualified imports) 

95 from langgraph.tools.ticket_dor import ticket_dor as _exec 

96 return _exec(step, state, invoke_agent) 

97 

98 

99def builtin_ticket_review(step: dict, state: dict, invoke_agent: Any) -> dict: 

100 try: 

101 from tools.ticket_review import ticket_review as _exec 

102 except ImportError: # pragma: no cover — main.py run (qualified imports) 

103 from langgraph.tools.ticket_review import ticket_review as _exec 

104 return _exec(step, state, invoke_agent) 

105 

106 

107def builtin_ticket_context(step: dict, state: dict) -> dict: 

108 try: 

109 from tools.ticket_context import ticket_context as _exec 

110 except ImportError: # pragma: no cover — main.py run (qualified imports) 

111 from langgraph.tools.ticket_context import ticket_context as _exec 

112 return _exec(step, state) 

113 

114 

115def builtin_ticket_lane(step: dict, state: dict) -> dict: 

116 try: 

117 from tools.ticket_lane import ticket_lane as _exec 

118 except ImportError: # pragma: no cover — main.py run (qualified imports) 

119 from langgraph.tools.ticket_lane import ticket_lane as _exec 

120 return _exec(step, state) 

121 

122 

123def builtin_ticket_approval(step: dict, state: dict) -> dict: 

124 try: 

125 from tools.ticket_approval import ticket_approval as _exec 

126 except ImportError: # pragma: no cover — main.py run (qualified imports) 

127 from langgraph.tools.ticket_approval import ticket_approval as _exec 

128 return _exec(step, state) 

129 

130 

131def builtin_ticket_search(step: dict, state: dict) -> dict: 

132 try: 

133 from tools.ticket_search import ticket_search as _exec 

134 except ImportError: # pragma: no cover — main.py run (qualified imports) 

135 from langgraph.tools.ticket_search import ticket_search as _exec 

136 return _exec(step, state) 

137 

138 

139def builtin_ticket_create(step: dict, state: dict) -> dict: 

140 try: 

141 from tools.ticket_create import ticket_create as _exec 

142 except ImportError: # pragma: no cover — main.py run (qualified imports) 

143 from langgraph.tools.ticket_create import ticket_create as _exec 

144 return _exec(step, state) 

145 

146 

147def _invoker_bound(builtin_fn: Any, executor: Any) -> Any: 

148 """Bind a decision builtin to its executor's LLM invoker. 

149 

150 The engine calls builtins with the (step, state) signature; the 

151 TICKET decision builtins additionally need the pipeline's 

152 invoke_agent_fn, which GraphBuilder stores on the BuiltinExecutor 

153 (invoke_agent_fn). The wrapper reads the attribute on every call, so 

154 the binding order (executor setup vs. lazy module registration) does 

155 not matter. 

156 """ 

157 

158 def wrapper(step: dict, state: dict) -> dict: 

159 return builtin_fn(step, state, executor.invoke_agent_fn) 

160 

161 return wrapper 

162 

163 

164def register_all(executor: Any) -> None: 

165 executor.register("ticket-publish", builtin_ticket_publish) 

166 executor.register("ticket-context", builtin_ticket_context) 

167 executor.register("ticket-lane", builtin_ticket_lane) 

168 executor.register("ticket-dor", _invoker_bound(builtin_ticket_dor, executor)) 

169 executor.register("ticket-review", _invoker_bound(builtin_ticket_review, executor)) 

170 executor.register("ticket-search", builtin_ticket_search) 

171 executor.register("ticket-create", builtin_ticket_create) 

172 executor.register("ticket-approval", builtin_ticket_approval)