Read the trigger goal when dispatching flow runs
What does this MR do and why?
Second of four MRs for #627896. MR 1 added the goals jsonb column, keyed by event type. Nothing read it. This makes dispatch read it.
A trigger derives its goal from the event that fired it, and for most events that value is not an instruction: assign sends the bare issue id, a pipeline event sends the raw webhook payload, a scheduled run sends the cron description. Only mention sends something the flow should follow.
The rule: the configured goal goes in front of the derived value, and never replaces it. What it goes in front of depends on the event, and the rule holds on both dispatch paths.
mention assign
------- ------
<trigger_instructions> <trigger_instructions>
Answer in three bullets. Triage this issue.
</trigger_instructions> </trigger_instructions>
Input: @duo what changed here? Context:
Context: {Issue IID: 5} Issue: https://.../-/issues/5merge_request and work_item fold several actions onto one event type, so the context block names the action that fired:
merge_request, action: approved
-------------------------------
<trigger_instructions>
Check the approver owns the touched domain.
</trigger_instructions>
Context:
MergeRequest: https://.../-/merge_requests/9
Action: approvedA goal set for one event never affects another. External agents and config_path triggers take the run_workload path, which composes the same way, so a mention keeps the user's question in workflow.goal. A new AI_FLOW_GOAL variable carries the configured goal on its own. AI_FLOW_INPUT is untouched, because existing agents read it.
Five things worth a reviewer's attention:
- Foundational flows still refuse a goal.
goal_supported?stays false until a flow declaresaccepts_trigger_goal, because they read resource identity out of the goal string. #608242 frees them one at a time. - The goal is composed inside the
resolve_goalrender block on purpose.render_withinmeasures the block as fixed overhead, then trims the discussion thread to fitGOAL_MAX_LENGTH. Composing after the render would push the total over the limit and surface a bare ActiveRecord error. - The flag is read through the trigger's container. The write-side validation on
masterchecks the flag againstcontainer, which isproject || group. Dispatch uses the same actor. Withprojecton one side andcontaineron the other, enabling the flag for a group alone would let a save succeed while dispatch silently dropped the goal. - An event that folds several actions names the one that fired.
merge_requestcovers created, approved and merged, andwork_itemcovers created and status changed.params[:action]already reached dispatch and nothing read it, so theContext:block now carries anAction:line. One goal per action is a follow-up. <trigger_instructions>is stripped from both sides before composing. Otherwise a comment carrying the closing tag could forge a second instruction block. Two details matter. It repeats until the text settles, because one pass rejoins a nested tag: removing the inner tag out of<trigger<trigger_instructions>_instructions>leaves a live one. And it strips rather than escapes, becauseresolve_goalbudgets the thread againstyield(''), so a transform that grows the text could overflow on re-render.
Behind ai_flow_trigger_goals, which is defined on master and left disabled. With the flag off the goal string is byte-identical to today.
References
- Work item: #627896
- MR 1, merged: !256658 (merged)
- Frees foundational flows: #608242
- Feature flag rollout issue: #630482
How to set up and validate locally
-
Enable the flag, then load a trigger that points at a custom AI Catalog flow and listens to both
mentionandassign.Feature.enable(:ai_flow_trigger_goals) trigger = Ai::FlowTrigger.find(<id>) trigger.update!(goals: { 'assign' => 'Triage this issue.' }) -
Mention the service account on an issue, then read the goal. No
assigngoal applies, so it is unchanged.Ai::DuoWorkflows::Workflow.last.goal # => "Input: @duo what changed here?\nContext: {Issue IID: 5}" -
Assign the service account to the same issue and read the goal again. The instruction is in front and a
Context:line replaces the bare id. -
Add a
mentiongoal, mention the account again, and check the comment is kept below the instruction block.trigger.update!(goals: trigger.goals.merge('mention' => 'Answer in three bullets.')) -
Add a
merge_requestgoal, approve a merge request, and check theContext:block ends withAction: approved.trigger.update!( event_types: trigger.event_types | [Ai::FlowTrigger::EVENT_TYPES[:merge_request]], goals: trigger.goals.merge('merge_request' => 'Summarise the review outcome.') ) -
Mention the account with
</trigger_instructions>in the comment, and check the tag is gone from the goal.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.