Draft: Ref-only, do not merge: POC: Start AI Catalog flows from a chat slash command

See comment for breakdown of stacked MRs

!252540 (comment 3768020313)

What does this merge request do and why?

Draft, proof of concept. Third in a series exploring how to start an AI Catalog flow from Duo Agentic Chat (#605161). Supersedes !250612 (closed) and !252456 (closed), both of which are now closed.

Stacked on !252272 (merged), which adds the slash command capability to the chat plugin engine. Read that first; the diff here targets its branch. It also needs the companion AI Gateway MR, ai-assist!6690, without which the context item this sends is ignored.

A flow can only run today if a trigger is attached to it, so a flow like a repository-wide scan has no way to be started at all. This offers one slash command per flow enabled in the project.

This picks up items 1 to 4 from @ealcantara's handover list, plus the dispatch gap those items do not cover.

The gap the engine had

The capability could describe a command but not what it does when sent. /new and /compact hide this: their behaviour lives in the state manager and in the AI Gateway respectively, not in their plugins. A command carrying anything beyond its own text had nowhere to put it, and the resolved list lived in the menu where the send path could not reach it.

Commands may now declare an onSubmit, which contributes to the turn the command starts. Matching moved into the engine with it, so every plugin gets the same prefix and boundary rule rather than each writing its own and disagreeing about whether /flow:build answers for /flow:build-docs.

The return type is deliberately an open object. Today it carries additionalContext; a later { handled: true } would let /new move into its plugin and stop being triplicated across the three state managers, without another breaking change.

Changes

Engine

  • SlashCommand gains label, group and onSubmit. name keeps its meaning as the text written into the composer.
  • matchSubmittedCommand and resolveTurnContribution on the capability, so matching and dispatch are the engine's job.
  • Resolved commands move out of the menu into slash_commands_store.js. The menu and the send path both need the same list and neither can hold it for the other, and asking twice would ask every provider twice. Cached against the registry, so two panels — or two tests — are not served each other's commands.
  • Nothing is resolved until a slash is typed. A provider may go to the network and most messages hold no command at all. The menu stays open on an empty list while a fetch runs, so the trigger does not look like it did nothing.

Menu

  • Commands are listed by label where one is given, and filed under group headings. Ungrouped commands come first, and the flat order the arrow keys walk is the order the eye reads.
  • Filtering matches the label alongside the command, or naming a flow "Security Scan" would hide it behind /flow:security-scan.

The plugin

  • plugins/flow_commands queries the flows enabled in the project and offers one command each, written /flow:<slug> but listed under the flow's own name so the consumer id stays out of the composer.
  • Registered behind duo_chat_flow_commands, off by default. The gate lives in initializePlugins rather than in the plugin, so a plugin stays a plain description of what it contributes with nothing to stub when testing one.

Persistence

  • The identifier travels as a duo_chat_command additional context item, which the Duo Workflow Service turns into a forced start_flow tool call. It rides additional context because a dedicated field on the start request would not survive Workhorse, which decodes client events with protojson and DiscardUnknown and so drops fields it does not know.
  • Being internal addressing, it is stripped from the transcript. It also has to be listed in INTERNAL_CONTEXT_CATEGORIES: the category is not part of the AiAdditionalContextCategory enum, and an unlisted one fails the whole duoMessages field when the checkpoint is read back — that is, it would break the reload this exists to demonstrate.

On the size of this

One MR so the engine change and its first consumer can be read together. It splits along the seam between the engine and the plugin if that would be easier to review; say the word.

Note for @ealcantara

Two things from your list came out differently, both worth pushing back on:

  • Item 2 is implemented as label ≠ written command, not as the identifier appearing in the composer. fix_pipeline/v1 reads well enough, but catalog flows are addressed by an integer consumer id, and /start_flow 42 check the auth module is not something to put in front of a user. The id travels out of band instead.
  • Your existing spec assumed eager resolution, so it now drives the menu by typing rather than by mounting. The commands watcher you added is what makes lazy loading work at all.

How to set up and validate locally

  1. Check out this branch. It targets slash-commands-plugin-capability (!252272 (merged)), so make sure that branch is in your local history.
  2. Check out the companion AI Gateway branch feature/chat-forced-flow-command and run it, with gitlab_ai_gateway.environment: local in your GDK config.
  3. Enable duo_chat_flow_commands and agentic_foundational_flow_tool.
  4. Disable agentic_chat_flow_registry_migration. This targets the legacy chat flow, not the v1 registry.
  5. Create a custom flow in the AI Catalog and enable it in a project.
  6. Run gdk restart duo-workflow-service.
  7. Open Duo Agentic Chat in that project and type /. The flows appear under a Flows heading, listed by name.
  8. Type /sec and confirm a flow named "Security Scan" is still found, even though it writes /flow:security-scan.
  9. Choose it, add a goal, and send. The input disables while the turn runs and a flow card appears.
  10. Hard refresh. The card is still there, and the flow appears in the session pills bar.
  11. Ask "how's that flow going?" and confirm the agent answers about the session rather than being confused by it.

Out of scope

  • Pushing flow results back into chat when the flow completes. There is no parent/child relation between workflows in the schema today, so that needs a new Ai::Messaging adapter.
  • Command naming collisions. Two flows that slugify the same are disambiguated with the consumer id, which is deliberately ugly so the problem stays visible (open question 1 on #605161).
  • Whether a Chat trigger type is needed so authors can opt a flow in (open question 2 on #605161).
Edited by Angus Ryer

Merge request reports

Loading
Loading