doc/development/ai_features/duo_agent_platform.md
This guide explains how to work with the GitLab Duo Agent Platform.
The GitLab Duo Agent Platform is a Single Page Application (SPA) built with Vue.js that provides a unified interface for AI-powered automation features. The platform uses a scoped routing system that allows multiple navigation items to coexist under the /automate path.
The platform is architected with a flexible namespace system that allows the same frontend infrastructure to be reused across different contexts (projects, groups, etc.) while providing context-specific functionality through a component mapping system.
The namespace system is built around a central mapping mechanism that:
The main entry point is located at:
ee/app/assets/javascripts/pages/projects/duo_agents_platform/index.js
This file imports and initializes the platform:
import { initDuoAgentsPlatformProjectPage } from 'ee/ai/duo_agents_platform/namespace/project';
initDuoAgentsPlatformProjectPage();
graph TD
A[Entry Point] --> B[initDuoAgentsPlatformPage]
B --> C[Extract Namespace Data]
C --> D[Create Router with Namespace]
D --> E[Component Mapping]
E --> F[Namespace-Specific Component]
F --> G[GraphQL Query with Props]
G --> H[Rendered UI]
I[Dataset Properties] --> C
J[Namespace Constant] --> E
K[Component Mappings] --> E
The GitLab Duo Agent Platform uses a router-driven navigation system where the Vue Router configuration directly drives the breadcrumb navigation. The key insight is that the router structure in ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js determines both the URL structure and the breadcrumb hierarchy.
The system works through these interconnected components:
meta.text properties define breadcrumb labelsduo_agents_platform_breadcrumbs.vue automatically generates breadcrumbs from matched routesinjectVueAppBreadcrumbs() in index.js connects the router to the breadcrumb systemLooking at the current router structure in ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js:
routes: [
{
component: NestedRouteApp, // Simple <router-view /> wrapper
path: '/agent-sessions',
meta: {
text: s__('DuoAgentsPlatform|Sessions'), // This becomes a breadcrumb
},
children: [
{
name: AGENTS_PLATFORM_INDEX_ROUTE,
path: '', // Matches /agent-sessions exactly
component: AgentsPlatformIndex,
},
{
name: AGENTS_PLATFORM_NEW_ROUTE,
path: 'new', // Matches /agent-sessions/new
component: AgentsPlatformNew,
meta: {
text: s__('DuoAgentsPlatform|New'), // This becomes a breadcrumb
},
},
// ...
],
},
];
The breadcrumb component (duo_agents_platform_breadcrumbs.vue) works by:
this.$route.matchedmeta.text from each matched route// From duo_agents_platform_breadcrumbs.vue
const matchedRoutes = (this.$route?.matched || [])
.map((route) => {
return {
text: route.meta?.text, // Uses meta.text for breadcrumb label
to: { path: route.path },
};
})
.filter((r) => r.text); // Only routes with meta.text become breadcrumbs
To add a new top-level navigation item (like "Your Feature"), you need to add a new route tree to the router:
File: ee/app/assets/javascripts/ai/duo_agents_platform/router/constants.js
// Add route name constants for your feature
export const AGENTS_PLATFORM_YOUR_FEATURE_INDEX = 'your_feature_index';
export const AGENTS_PLATFORM_YOUR_FEATURE_NEW = 'your_feature_new';
export const AGENTS_PLATFORM_YOUR_FEATURE_SHOW = 'your_feature_show';
File: ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js
import YourFeatureIndex from '../pages/your_feature/your_feature_index.vue';
import YourFeatureNew from '../pages/your_feature/your_feature_new.vue';
import YourFeatureShow from '../pages/your_feature/your_feature_show.vue';
export const createRouter = (base, namespace) => {
return new VueRouter({
base,
mode: 'history',
routes: [
// Existing agent-sessions routes
{
component: NestedRouteApp,
path: '/agent-sessions',
meta: {
text: s__('DuoAgentsPlatform|Sessions'),
},
children: [
{
name: AGENTS_PLATFORM_INDEX_ROUTE,
path: '',
component: getNamespaceIndexComponent(namespace),
},
// ... existing children
],
},
// NEW: Your feature routes
{
component: NestedRouteApp,
path: '/your-feature', // This becomes the URL path
meta: {
text: s__('DuoAgentsPlatform|Your Feature'), // This becomes the breadcrumb
},
children: [
{
name: AGENTS_PLATFORM_YOUR_FEATURE_INDEX,
path: '', // Matches /your-feature exactly
component: YourFeatureIndex,
},
{
name: AGENTS_PLATFORM_YOUR_FEATURE_NEW,
path: 'new', // Matches /your-feature/new
component: YourFeatureNew,
meta: {
text: s__('DuoAgentsPlatform|New'), // Breadcrumb: "Your Feature > New"
},
},
{
name: AGENTS_PLATFORM_YOUR_FEATURE_SHOW,
path: ':id(\\d+)', // Matches /your-feature/123
component: YourFeatureShow,
// No meta.text - will use route param as breadcrumb
},
],
},
{ path: '*', redirect: '/agent-sessions' },
],
});
};
The existing wildcard route in ee/config/routes/project.rb should handle your new paths:
scope :automate do
get '/(*vueroute)' => 'duo_agents_platform#show', as: :automate, format: false
end
Important: If you're adding a sidebar menu item (Step 4), you must add a named route helper:
scope :automate do
get '/(*vueroute)' => 'duo_agents_platform#show', as: :automate, format: false
# Named routes for sidebar menu helpers
get 'agent-sessions', to: 'duo_agents_platform#show', as: :automate_agent_sessions, format: false
get 'your-feature', to: 'duo_agents_platform#show', as: :automate_your_features, format: false
end
[!note] Use plural form for the route name (e.g.,
automate_your_features) to match the existing pattern and ensure the Rails path helper is generated correctly.
File: ee/lib/sidebars/projects/super_sidebar_menus/duo_agents_menu.rb
Add the menu item to the configure_menu_items method and create the corresponding menu item method:
override :configure_menu_items
def configure_menu_items
add_item(duo_agents_runs_menu_item)
add_item(duo_agents_your_feature_menu_item) # Add your new menu item
true
end
private
def duo_agents_your_feature_menu_item
::Sidebars::MenuItem.new(
title: s_('Your Feature'),
link: project_automate_your_features_path(context.project), # Note: plural 'features'
active_routes: { controller: :duo_agents_platform },
item_id: :agents_your_feature
)
end
File: ee/app/assets/javascripts/ai/duo_agents_platform/pages/your_feature/your_feature_index.vue
<script>
export default {
name: 'YourFeatureIndex',
};
</script>
<template>
<div>
<h1>Your Feature</h1>
</div>
</template>
To add a new namespace (e.g., for groups):
// ee/app/assets/javascripts/ai/duo_agents_platform/constants.js
export const AGENT_PLATFORM_GROUP_PAGE = 'group';
ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/
├── index.js
├── group_agents_platform_index.vue
└── graphql/
└── queries/
└── get_group_agent_flows.query.graphql
// ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/index.js
import { initDuoAgentsPlatformPage } from '../../index';
import { AGENT_PLATFORM_GROUP_PAGE } from '../../constants';
export const initDuoAgentsPlatformGroupPage = () => {
initDuoAgentsPlatformPage({
namespace: AGENT_PLATFORM_GROUP_PAGE,
namespaceDatasetProperties: ['groupPath', 'groupId'],
});
};
<!-- ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/group_agents_platform_index.vue -->
<script>
import getGroupAgentFlows from './graphql/queries/get_group_agent_flows.query.graphql';
import DuoAgentsPlatformIndex from '../../pages/index/duo_agents_platform_index.vue';
export default {
components: { DuoAgentsPlatformIndex },
inject: ['groupPath'], // Group-specific injection
apollo: {
workflows: {
query: getGroupAgentFlows, // Group-specific query
variables() {
return {
groupPath: this.groupPath,
// ...
};
},
// ...
},
},
};
</script>
<template>
<duo-agents-platform-index
:is-loading-workflows="isLoadingWorkflows"
:workflows="workflows"
:workflows-page-info="workflowsPageInfo"
:workflow-query="$apollo.queries.workflows"
/>
</template>
// ee/app/assets/javascripts/ai/duo_agents_platform/router/utils.js
import { AGENT_PLATFORM_PROJECT_PAGE, AGENT_PLATFORM_GROUP_PAGE } from '../constants';
import ProjectAgentsPlatformIndex from '../namespace/project/project_agents_platform_index.vue';
import GroupAgentsPlatformIndex from '../namespace/group/group_agents_platform_index.vue';
export const getNamespaceIndexComponent = (namespace) => {
const componentMappings = {
[AGENT_PLATFORM_PROJECT_PAGE]: ProjectAgentsPlatformIndex,
[AGENT_PLATFORM_GROUP_PAGE]: GroupAgentsPlatformIndex, // New mapping
};
return componentMappings[namespace];
};
// ee/app/assets/javascripts/pages/groups/duo_agents_platform/index.js
import { initDuoAgentsPlatformGroupPage } from 'ee/ai/duo_agents_platform/namespace/group';
initDuoAgentsPlatformGroupPage();
The breadcrumb system automatically generates navigation from the router structure:
meta.text become breadcrumb segmentsmeta.text extend the breadcrumb chainmeta.text use route parameters (like :id) as breadcrumb textEach top-level navigation item gets its own URL namespace:
/automate/agent-sessions, /automate/agent-sessions/new, /automate/agent-sessions/123/automate/your-feature, /automate/your-feature/new, /automate/your-feature/456The platform uses a consistent nested route pattern:
<router-view /> wrapper for child routesThe existing agent sessions feature demonstrates this pattern:
{
component: NestedRouteApp, // Renders child routes
path: '/agent-sessions', // URL: /automate/agent-sessions
meta: {
text: s__('DuoAgentsPlatform|Sessions'), // Breadcrumb: "Sessions"
},
children: [
{
name: AGENTS_PLATFORM_INDEX_ROUTE,
path: '', // URL: /automate/agent-sessions
component: AgentsPlatformIndex, // No additional breadcrumb
},
{
name: AGENTS_PLATFORM_NEW_ROUTE,
path: 'new', // URL: /automate/agent-sessions/new
component: AgentsPlatformNew,
meta: {
text: s__('DuoAgentsPlatform|New'), // Breadcrumb: "Sessions > New"
},
},
{
name: AGENTS_PLATFORM_SHOW_ROUTE,
path: ':id(\\d+)', // URL: /automate/agent-sessions/123
component: AgentsPlatformShow, // Breadcrumb: "Sessions > 123"
// No meta.text - uses :id parameter as breadcrumb
},
],
}
This creates the breadcrumb hierarchy:
/automate/agent-sessions → "Automate > Sessions"/automate/agent-sessions/new → "Automate > Sessions > New"/automate/agent-sessions/123 → "Automate > Sessions > 123"ee/app/assets/javascripts/pages/projects/duo_agents_platform/index.jsee/app/assets/javascripts/ai/duo_agents_platform/index.jsee/app/assets/javascripts/ai/duo_agents_platform/router/index.jsee/app/assets/javascripts/ai/duo_agents_platform/router/utils.jsee/app/assets/javascripts/ai/duo_agents_platform/constants.jsee/app/assets/javascripts/ai/duo_agents_platform/utils.jsee/app/assets/javascripts/ai/duo_agents_platform/namespace/project/meta.text properties to define breadcrumb hierarchy/your-feature, not /yourFeature)AGENTS_PLATFORM_YOUR_FEATURE_INDEX)pages/[feature]/ directoriess__()) for all meta.text valuesNestedRouteAppnamespace/meta.text propertiesee/config/routes/project.rb existsactive_routes$route.matched to see which routes are being matchedconsole.log(this.$route.matched) in the breadcrumb componentTo generate fake flows to test out the platform, you can run
ee/lib/tasks/gitlab/duo_workflow/duo_workflow.rake Rake task.
Example to make 50 flows, 20 made by the specified user in specific project
bundle exec rake "gitlab:duo_workflow:populate[50,20,[email protected],gitlab-org/gitlab-test]