Add agent type to Duo Workflow session artifacts

What does this MR do and why?

The AI audit report table needs the agent type per session. PostgreSQL is the fallback store when ClickHouse is not enabled, so the value gets denormalized onto duo_workflow_session_artifacts, the same way workflow_definition and model_used already are (decision by the PM on the issue). This adds a nullable agent_type column plus a backfill, copies the value in SessionArtifact.sync_from_workflow!, and exposes agentType on the DuoWorkflowSessionArtifact GraphQL type (experiment, 19.4).

Null means the session ran on the internal Duo Agent Platform. Values like claude-code and opencode come from the external agents session API, which validates them against Ai::ExternalAgents::AgentIdentity::AGENT_TYPES. The ClickHouse read path needs no migration: it reads siphon_duo_workflows_workflows.agent_type, which already exists.

Resolves https://gitlab.com/gitlab-org/gitlab/-/issues/623440

Database

db/migrate/20260827164820_add_agent_type_to_duo_workflow_session_artifacts.rb: nullable text, 50 limit, matching duo_workflows_workflows.agent_type. NULL means an internal Duo Agent Platform session.

db/post_migrate/20260903145403_queue_backfill_agent_type_on_duo_workflow_session_artifacts.rb: queues BackfillAgentTypeOnDuoWorkflowSessionArtifacts on gitlab_main_org, cursor on id, batch 1000, sub-batch 100. Replaces the earlier synchronous update_column_in_batches backfill, since table_size: small only describes GitLab.com.

WITH sub_batch AS MATERIALIZED (
  SELECT "duo_workflow_session_artifacts"."id" FROM "duo_workflow_session_artifacts"
  WHERE ("duo_workflow_session_artifacts"."id") >= (<start>) AND ("duo_workflow_session_artifacts"."id") <= (<end>)
  ORDER BY "duo_workflow_session_artifacts"."id" ASC LIMIT 100
)
UPDATE duo_workflow_session_artifacts
SET agent_type = duo_workflows_workflows.agent_type
FROM duo_workflows_workflows
WHERE duo_workflow_session_artifacts.id IN (SELECT id FROM sub_batch)
  AND duo_workflows_workflows.id = duo_workflow_session_artifacts.workflow_id
  AND duo_workflow_session_artifacts.agent_type IS NULL
  AND duo_workflows_workflows.agent_type IS NOT NULL

Plans, on a clone seeded with the newest 100k rows from duo_workflows_workflows (the table is empty on GitLab.com):

Per-sub-batch UPDATE (sub_batch_size 100, cold cache) https://console.postgres.ai/gitlab/projects/gitlab-production-main-v2/sessions/56219/commands/160376

Batch-bounds lookup (batch_size 1000) https://console.postgres.ai/gitlab/projects/gitlab-production-main-v2/sessions/56219/commands/160379

Only rows where the artifact's agent_type is NULL and the workflow's is NOT NULL are touched, so re-runs are no-ops and populated values are never overwritten. workflow_id is ON DELETE CASCADE, no dangling rows. Both tables are gitlab_main_org.

Rollback: roll back the queue migration first so no sub-batch is left running, then drop the column via the first migration's down. Backfilled values can't be told apart from app-written ones, so nothing is reverted selectively.

How to validate

GDK Rails console on this branch.

u = User.find_by_username('root')
p = Project.last
ext = Ai::DuoWorkflows::Workflow.create!(project: p, user: u, goal: 'demo',
  workflow_definition: 'external_agent', environment: :external, agent_type: 'claude-code')
dap = Ai::DuoWorkflows::Workflow.create!(project: p, user: u, goal: 'demo',
  workflow_definition: 'software_development', environment: :web)
[ext, dap].each { |w| Ai::DuoWorkflows::SessionArtifact.sync_from_workflow!(w) }

q = <<~G
  { group(fullPath: "#{p.group.full_path}") { duoWorkflowSessionArtifacts(first: 5) { nodes { id agentType } } } }
G
GitlabSchema.execute(q, context: { current_user: u })
  .dig('data', 'group', 'duoWorkflowSessionArtifacts', 'nodes')

The external session node returns "agentType" => "claude-code", the DAP node returns nil. Toggling ApplicationSetting.current.update!(use_clickhouse_for_analytics: true) and re-running returns the same values through the ClickHouse path (verified on GDK with both settings).

Follow-ups

  • The session artifact export JSON (SessionArtifactExportService) does not include agent_type yet. Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/627374.
  • Frontend consumption of the field is separate work, owned by the frontend counterpart on the issue.

MR acceptance checklist: Evaluate this MR against the MR acceptance checklist.

Edited by Andrew Jung

Merge request reports

Loading
Loading