feat(investigations): Add agentic orchestration workflows - #122949
feat(investigations): Add agentic orchestration workflows#122949arslnb wants to merge 8 commits into
Conversation
|
🚨 Warning: This pull request contains Frontend and Backend changes! It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently. Have questions? Please ask in the |
Story previewsPreview the stories changed in this PR on the Vercel deployment:
Preview deployment: https://sentry-9e6o2oxxv.sentry.dev |
📊 Type Coverage Diff
🔍 9 new type safety issues introducedNon-null assertions (
Type assertions (
This is informational only and does not block the PR. |
|
This PR has a migration; here is the generated SQL for for --
-- Add field producing_seer_run_id to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "producing_seer_run_id" bigint NULL;
--
-- Add field report_revision to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "report_revision" integer NULL CHECK ("report_revision" >= 0);
--
-- Add field stable_agent_key to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "stable_agent_key" varchar(128) NULL;
--
-- Create model InvestigationOrchestrationRun
--
CREATE TABLE "investigations_investigationorchestrationrun" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "date_updated" timestamp with time zone NOT NULL, "date_added" timestamp with time zone NOT NULL, "seer_run_id" bigint NULL UNIQUE, "schema_version" integer DEFAULT 1 NOT NULL CHECK ("schema_version" >= 0), "workflow_version" integer DEFAULT 1 NOT NULL CHECK ("workflow_version" >= 0), "generation" integer DEFAULT 1 NOT NULL CHECK ("generation" >= 0), "phase" varchar(32) DEFAULT 'intake' NOT NULL, "status" varchar(32) DEFAULT 'pending' NOT NULL, "source" jsonb DEFAULT '{}'::jsonb NOT NULL, "projection" jsonb DEFAULT '{}'::jsonb NOT NULL, "notebook_revision" integer DEFAULT 0 NOT NULL CHECK ("notebook_revision" >= 0), "last_event_sequence" integer DEFAULT 0 NOT NULL CHECK ("last_event_sequence" >= 0), "heartbeat_at" timestamp with time zone NULL, "error" jsonb NULL, "investigation_id" bigint NOT NULL UNIQUE);
--
-- Create model InvestigationOrchestrationEvent
--
CREATE TABLE "investigations_investigationorchestrationevent" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "date_updated" timestamp with time zone NOT NULL, "date_added" timestamp with time zone NOT NULL, "event_id" uuid NOT NULL, "sequence" integer NOT NULL CHECK ("sequence" >= 0), "type" varchar(64) NOT NULL, "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, "application_status" varchar(32) DEFAULT 'pending' NOT NULL, "error" jsonb NULL, "applied_at" timestamp with time zone NULL, "orchestration_run_id" bigint NOT NULL);
--
-- Create model InvestigationOrchestrationCommand
--
CREATE TABLE "investigations_investigationorchestrationcommand" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "date_updated" timestamp with time zone NOT NULL, "date_added" timestamp with time zone NOT NULL, "request_id" uuid NOT NULL, "actor_id" bigint NULL, "expected_workflow_version" integer NOT NULL CHECK ("expected_workflow_version" >= 0), "resulting_workflow_version" integer NULL CHECK ("resulting_workflow_version" >= 0), "type" varchar(64) NOT NULL, "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, "status" varchar(32) DEFAULT 'accepted' NOT NULL, "error" jsonb NULL, "orchestration_run_id" bigint NOT NULL);
--
-- Add field orchestration_run to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "orchestration_run_id" bigint NULL;
--
-- Create index investigati_orchest_5b44a3_idx on field(s) orchestration_run, report_revision, stable_agent_key of model investigationblock
--
CREATE INDEX CONCURRENTLY "investigati_orchest_5b44a3_idx" ON "investigations_investigationblock" ("orchestration_run_id", "report_revision", "stable_agent_key");
--
-- Create constraint invest_unique_report_block_key on model investigationblock
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_unique_report_block_key" ON "investigations_investigationblock" ("orchestration_run_id", "report_revision", "stable_agent_key") WHERE "stable_agent_key" IS NOT NULL;
--
-- Create index investigati_status_9ed759_idx on field(s) status, heartbeat_at of model investigationorchestrationrun
--
CREATE INDEX CONCURRENTLY "investigati_status_9ed759_idx" ON "investigations_investigationorchestrationrun" ("status", "heartbeat_at");
--
-- Create index investigati_phase_8930ff_idx on field(s) phase, -date_updated of model investigationorchestrationrun
--
CREATE INDEX CONCURRENTLY "investigati_phase_8930ff_idx" ON "investigations_investigationorchestrationrun" ("phase", "date_updated" DESC);
--
-- Create index investigati_orchest_c79498_idx on field(s) orchestration_run, application_status, sequence of model investigationorchestrationevent
--
CREATE INDEX CONCURRENTLY "investigati_orchest_c79498_idx" ON "investigations_investigationorchestrationevent" ("orchestration_run_id", "application_status", "sequence");
--
-- Create constraint invest_orch_unique_event_id on model investigationorchestrationevent
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_orch_unique_event_id" ON "investigations_investigationorchestrationevent" ("orchestration_run_id", "event_id");
ALTER TABLE "investigations_investigationorchestrationevent" ADD CONSTRAINT "invest_orch_unique_event_id" UNIQUE USING INDEX "invest_orch_unique_event_id";
--
-- Create constraint invest_orch_unique_event_sequence on model investigationorchestrationevent
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_orch_unique_event_sequence" ON "investigations_investigationorchestrationevent" ("orchestration_run_id", "sequence");
ALTER TABLE "investigations_investigationorchestrationevent" ADD CONSTRAINT "invest_orch_unique_event_sequence" UNIQUE USING INDEX "invest_orch_unique_event_sequence";
--
-- Create index investigati_orchest_64e689_idx on field(s) orchestration_run, status, date_added of model investigationorchestrationcommand
--
CREATE INDEX CONCURRENTLY "investigati_orchest_64e689_idx" ON "investigations_investigationorchestrationcommand" ("orchestration_run_id", "status", "date_added");
--
-- Create constraint invest_orch_unique_command_request on model investigationorchestrationcommand
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_orch_unique_command_request" ON "investigations_investigationorchestrationcommand" ("orchestration_run_id", "request_id");
ALTER TABLE "investigations_investigationorchestrationcommand" ADD CONSTRAINT "invest_orch_unique_command_request" UNIQUE USING INDEX "invest_orch_unique_command_request";
ALTER TABLE "investigations_investigationorchestrationrun" ADD CONSTRAINT "investigations_inves_investigation_id_27bb2b36_fk_investiga" FOREIGN KEY ("investigation_id") REFERENCES "investigations_investigation" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationorchestrationrun" VALIDATE CONSTRAINT "investigations_inves_investigation_id_27bb2b36_fk_investiga";
ALTER TABLE "investigations_investigationorchestrationevent" ADD CONSTRAINT "investigations_inves_orchestration_run_id_a646a18e_fk_investiga" FOREIGN KEY ("orchestration_run_id") REFERENCES "investigations_investigationorchestrationrun" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationorchestrationevent" VALIDATE CONSTRAINT "investigations_inves_orchestration_run_id_a646a18e_fk_investiga";
CREATE INDEX CONCURRENTLY "investigations_investigati_orchestration_run_id_a646a18e" ON "investigations_investigationorchestrationevent" ("orchestration_run_id");
ALTER TABLE "investigations_investigationorchestrationcommand" ADD CONSTRAINT "investigations_inves_orchestration_run_id_9613235b_fk_investiga" FOREIGN KEY ("orchestration_run_id") REFERENCES "investigations_investigationorchestrationrun" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationorchestrationcommand" VALIDATE CONSTRAINT "investigations_inves_orchestration_run_id_9613235b_fk_investiga";
CREATE INDEX CONCURRENTLY "investigations_investigati_actor_id_7438bdc7" ON "investigations_investigationorchestrationcommand" ("actor_id");
CREATE INDEX CONCURRENTLY "investigations_investigati_orchestration_run_id_9613235b" ON "investigations_investigationorchestrationcommand" ("orchestration_run_id");
ALTER TABLE "investigations_investigationblock" ADD CONSTRAINT "investigations_inves_orchestration_run_id_c90475ca_fk_investiga" FOREIGN KEY ("orchestration_run_id") REFERENCES "investigations_investigationorchestrationrun" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationblock" VALIDATE CONSTRAINT "investigations_inves_orchestration_run_id_c90475ca_fk_investiga";
CREATE INDEX CONCURRENTLY "investigations_investigationblock_orchestration_run_id_c90475ca" ON "investigations_investigationblock" ("orchestration_run_id"); |
Backend Test FailuresFailures on
|
| return active, False | ||
| latest_revision = Investigation.objects.filter( | ||
| organization=organization, | ||
| lineage_key=lineage_key, | ||
| ).aggregate(latest=Max("source_revision"))["latest"] | ||
| try: | ||
| investigation, _ = create_agentic_investigation( | ||
| organization=organization, | ||
| user_id=user_id, | ||
| title=title, | ||
| source=normalized_source, | ||
| orchestration_source=breached_metric_orchestration_source(normalized_source), | ||
| project_ids=project_ids, | ||
| filters=filters, | ||
| lineage_key=lineage_key, | ||
| source_revision=(latest_revision or 0) + 1, | ||
| ) | ||
| return investigation, True | ||
| except IntegrityError: | ||
| if attempt == 2: | ||
| active = Investigation.objects.filter( | ||
| organization=organization, | ||
| lineage_key=lineage_key, | ||
| status=InvestigationStatus.ACTIVE, | ||
| ).first() | ||
| if active is not None: | ||
| return active, False |
There was a problem hiding this comment.
Agentic lineage reuse returns investigation details without project-access check
When create_agentic_breached_metric_investigation reuses an active investigation, the create endpoint returns full InvestigationDetailsSerializer output without require_investigation_project_access, unlike template reuse. Callers who can resolve the metric source but lack access to every linked or execution-represented project can read that investigation's details.
Evidence
- This hunk returns an existing active investigation as
(active, False)on lineage hit (and again after the final IntegrityError retry). OrganizationInvestigationsIndexEndpoint.postcalls this helper for agenticmetric_open_periodcreates and, whencreatedis false, never callsrequire_investigation_project_accessbefore responding.- The same endpoint does enforce that check on template reuse (
if not created: require_investigation_project_access(...)). - The 200 response still serializes via
InvestigationDetailsSerializer, which includes source/filters/projectIds/parameters/blocks; single-investigation GET/PUT paths are otherwise gated by that same project-access helper.
Identified by Warden · security-review · WLC-BTC
| investigation, _ = create_agentic_investigation( | ||
| organization=organization, | ||
| user_id=user_id, | ||
| title=title, | ||
| source=normalized_source, | ||
| orchestration_source=breached_metric_orchestration_source(normalized_source), | ||
| project_ids=project_ids, | ||
| filters=filters, | ||
| lineage_key=lineage_key, | ||
| source_revision=(latest_revision or 0) + 1, | ||
| ) |
There was a problem hiding this comment.
Breached-metric agentic create always violates investigation_source_fields_complete
create_agentic_breached_metric_investigation passes source_revision while create_agentic_investigation leaves source_type=manual and source_key=NULL, so every insert fails the check constraint; the IntegrityError retry path then always raises after 3 attempts instead of creating the investigation.
Evidence
create_agentic_breached_metric_investigationcallscreate_agentic_investigation(..., lineage_key=..., source_revision=(latest_revision or 0) + 1).create_agentic_investigationonly setssource,filters,lineage_key, andsource_revision— notsource_typeorsource_key(defaults: MANUAL / NULL).- Model check
investigation_source_fields_completerequires MANUAL rows to have bothsource_keyandsource_revisionNULL, or non-MANUAL rows to have both set. - Template path in
create_template_investigationcorrectly setssource_type=BREACHED_METRICandsource_key=legacy_source_key; this agentic path does not, so Postgres raisesIntegrityErroron every create.
Identified by Warden · sentry-backend-bugs · A2Q-WPH
| if run.seer_run_id is not None and run.seer_run_id != seer_run_id: | ||
| raise InvestigationOrchestrationEventConflict("Run ID does not match.") | ||
| run.seer_run_id = seer_run_id | ||
| if authoritative or ( | ||
| generation >= run.generation |
There was a problem hiding this comment.
synchronize_orchestration_projection crashes when an orchestration run is deleted during dispatch
Treat a missing InvestigationOrchestrationRun as a normal no-op throughout the asynchronous dispatch path. The projection synchronizer uses a bare .get() after the Seer request, so deletion during that request window raises DoesNotExist and escapes both dispatch tasks. If the synchronizer returns a missing result, callers must stop before scheduling follow-up work or acknowledging the command; _mark_command_dispatch_acknowledged should also tolerate deletion after projection synchronization.
Evidence
synchronize_orchestration_projectionperformsselect_for_update().get(id=orchestration_run_id)without handlingInvestigationOrchestrationRun.DoesNotExist.- Both orchestration dispatch tasks load the run with
.filter(...).first(), call Seer outside the projection transaction, and then invoke the synchronizer using the stale ID. InvestigationDeletionTaskdeletes the related orchestration run viaInvestigationOrchestrationRunDeletionTask, so the row can disappear while the Seer request is in flight.- The command path has a second deletion window after synchronization:
_mark_command_dispatch_acknowledgeduses bare.get()calls, so merely returning from the synchronizer would not fully prevent the task failure. _mark_create_dispatch_failedalready treats a missing run as a no-op, demonstrating the intended behavior for asynchronous cleanup races.
Also found at 2 additional locations
src/sentry/tasks/seer/investigation.py:99-101src/sentry/tasks/seer/investigation.py:228-229
Identified by Warden · sentry-backend-bugs · 4EM-V9R
Investigations can now run as agent-managed workflows instead of a fixed cell sequence. Sentry persists orchestration runs and events, dispatches lifecycle and steering commands to Seer, projects agent-authored reports into notebook cells, and renders live hypothesis progress plus evidence previews. Users can monitor, cancel, retry, and steer active investigations while existing manual and template investigations keep their current behavior.
The branch is rebased onto current master and preserves the latest Explore routes, feedback entry point, collapsed evidence behavior, cell action menus, access controls, and shared Seer embed formats. The obsolete
react-mentionsdependency is no longer part of the branch;@xyflow/reactremains because it powers the workflow graph.This draft intentionally keeps the complete end-to-end implementation together for review. It spans backend/migrations and frontend code, so it must be split along the deployment boundary before landing, with persistence and APIs landing before the UI.