Draft: Ref-only, do not merge: POC: Start AI Catalog flows from a chat slash command
See comment for breakdown of stacked MRs
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
SlashCommandgainslabel,groupandonSubmit.namekeeps its meaning as the text written into the composer.matchSubmittedCommandandresolveTurnContributionon 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
labelwhere one is given, and filed undergroupheadings. 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_commandsqueries 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 ininitializePluginsrather 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_commandadditional context item, which the Duo Workflow Service turns into a forcedstart_flowtool call. It rides additional context because a dedicated field on the start request would not survive Workhorse, which decodes client events withprotojsonandDiscardUnknownand 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 theAiAdditionalContextCategoryenum, and an unlisted one fails the wholeduoMessagesfield 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/v1reads well enough, but catalog flows are addressed by an integer consumer id, and/start_flow 42 check the auth moduleis 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
commandswatcher you added is what makes lazy loading work at all.
How to set up and validate locally
- Check out this branch. It targets
slash-commands-plugin-capability(!252272 (merged)), so make sure that branch is in your local history. - Check out the companion AI Gateway branch
feature/chat-forced-flow-commandand run it, withgitlab_ai_gateway.environment: localin your GDK config. - Enable
duo_chat_flow_commandsandagentic_foundational_flow_tool. - Disable
agentic_chat_flow_registry_migration. This targets the legacy chat flow, not the v1 registry. - Create a custom flow in the AI Catalog and enable it in a project.
- Run
gdk restart duo-workflow-service. - Open Duo Agentic Chat in that project and type
/. The flows appear under a Flows heading, listed by name. - Type
/secand confirm a flow named "Security Scan" is still found, even though it writes/flow:security-scan. - Choose it, add a goal, and send. The input disables while the turn runs and a flow card appears.
- Hard refresh. The card is still there, and the flow appears in the session pills bar.
- 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::Messagingadapter. - 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
Chattrigger type is needed so authors can opt a flow in (open question 2 on #605161).