Coverage for core/langgraph/_builtins_ticket.py: 100%
53 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 05:49 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 05:49 +0000
1"""_builtins_ticket.py — TICKET-profile builtins (lazy-loaded, needs httpx).
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).
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.
19Decision builtins (ticket-dor / ticket-review) stop the run with
20outcome-coded TicketOutcome exceptions instead of returning state:
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 |
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
40from typing import Any
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 )
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 )
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
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)
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)
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)
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)
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)
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)
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)
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)
147def _invoker_bound(builtin_fn: Any, executor: Any) -> Any:
148 """Bind a decision builtin to its executor's LLM invoker.
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 """
158 def wrapper(step: dict, state: dict) -> dict:
159 return builtin_fn(step, state, executor.invoke_agent_fn)
161 return wrapper
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)