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/5

merge_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: approved

A 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 declares accepts_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_goal render block on purpose. render_within measures the block as fixed overhead, then trims the discussion thread to fit GOAL_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 master checks the flag against container, which is project || group. Dispatch uses the same actor. With project on one side and container on 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_request covers created, approved and merged, and work_item covers created and status changed. params[:action] already reached dispatch and nothing read it, so the Context: block now carries an Action: 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, because resolve_goal budgets the thread against yield(''), 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

How to set up and validate locally

  1. Enable the flag, then load a trigger that points at a custom AI Catalog flow and listens to both mention and assign.

    Feature.enable(:ai_flow_trigger_goals)
    trigger = Ai::FlowTrigger.find(<id>)
    trigger.update!(goals: { 'assign' => 'Triage this issue.' })
  2. Mention the service account on an issue, then read the goal. No assign goal applies, so it is unchanged.

    Ai::DuoWorkflows::Workflow.last.goal
    # => "Input: @duo what changed here?\nContext: {Issue IID: 5}"
  3. 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.

  4. Add a mention goal, 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.'))
  5. Add a merge_request goal, approve a merge request, and check the Context: block ends with Action: approved.

    trigger.update!(
      event_types: trigger.event_types | [Ai::FlowTrigger::EVENT_TYPES[:merge_request]],
      goals: trigger.goals.merge('merge_request' => 'Summarise the review outcome.')
    )
  6. 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.


🤖 AI-generated

Edited by Justin Ho Tuan Duong

Merge request reports

Loading
Loading