feat(hitl): add human in the loop block (#1832)

* fix(billing): should allow restoring subscription (#1728)

* fix(already-cancelled-sub): UI should allow restoring subscription

* restore functionality fixed

* fix

* Add pause resume block

* Add db schema

* Initial test passes

* Tests pass

* Execution pauses

* Snapshot serializer

* Ui checkpoint

* Works 1

* Pause resume simple v1

* Hitl block works in parallel branches without timing overlap

* Pending status to logs

* Pause resume ui link

* Big context consolidation

* HITL works in loops

* Fix parallels

* Reference blocks properly

* Fix tag dropdown and start block resolution

* Filter console logs for hitl block

* Fix notifs

* Fix logs page

* Fix logs page again

* Fix

* Checkpoint

* Cleanup v1

* Refactor v2

* Refactor v3

* Refactor v4

* Refactor v5

* Resume page

* Fix variables in loops

* Fix var res bugs

* Ui changes

* Approval block

* Hitl works e2e v1

* Fix tets

* Row level lock

---------

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
This commit is contained in:
Siddharth Ganesan
2025-11-06 15:59:28 -08:00
committed by GitHub
parent f9ce65eddf
commit 742d59f54d
90 changed files with 13498 additions and 1128 deletions

View File

@@ -0,0 +1,37 @@
CREATE TABLE "paused_executions" (
"id" text PRIMARY KEY NOT NULL,
"workflow_id" text NOT NULL,
"execution_id" text NOT NULL,
"execution_snapshot" jsonb NOT NULL,
"pause_points" jsonb NOT NULL,
"total_pause_count" integer NOT NULL,
"resumed_count" integer DEFAULT 0 NOT NULL,
"status" text DEFAULT 'paused' NOT NULL,
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
"paused_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"expires_at" timestamp
);
--> statement-breakpoint
CREATE TABLE "resume_queue" (
"id" text PRIMARY KEY NOT NULL,
"paused_execution_id" text NOT NULL,
"parent_execution_id" text NOT NULL,
"new_execution_id" text NOT NULL,
"context_id" text NOT NULL,
"resume_input" jsonb,
"status" text DEFAULT 'pending' NOT NULL,
"queued_at" timestamp DEFAULT now() NOT NULL,
"claimed_at" timestamp,
"completed_at" timestamp,
"failure_reason" text
);
--> statement-breakpoint
ALTER TABLE "custom_tools" ALTER COLUMN "workspace_id" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "paused_executions" ADD CONSTRAINT "paused_executions_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "resume_queue" ADD CONSTRAINT "resume_queue_paused_execution_id_paused_executions_id_fk" FOREIGN KEY ("paused_execution_id") REFERENCES "public"."paused_executions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "paused_executions_workflow_id_idx" ON "paused_executions" USING btree ("workflow_id");--> statement-breakpoint
CREATE INDEX "paused_executions_status_idx" ON "paused_executions" USING btree ("status");--> statement-breakpoint
CREATE UNIQUE INDEX "paused_executions_execution_id_unique" ON "paused_executions" USING btree ("execution_id");--> statement-breakpoint
CREATE INDEX "resume_queue_parent_status_idx" ON "resume_queue" USING btree ("parent_execution_id","status","queued_at");--> statement-breakpoint
CREATE INDEX "resume_queue_new_execution_idx" ON "resume_queue" USING btree ("new_execution_id");

File diff suppressed because it is too large Load Diff

View File

@@ -736,6 +736,13 @@
"when": 1761860659858,
"tag": "0105_glamorous_wrecking_crew",
"breakpoints": true
},
{
"idx": 106,
"version": "7",
"when": 1762371130884,
"tag": "0106_bitter_captain_midlands",
"breakpoints": true
}
]
}

View File

@@ -317,6 +317,58 @@ export const workflowExecutionLogs = pgTable(
})
)
export const pausedExecutions = pgTable(
'paused_executions',
{
id: text('id').primaryKey(),
workflowId: text('workflow_id')
.notNull()
.references(() => workflow.id, { onDelete: 'cascade' }),
executionId: text('execution_id').notNull(),
executionSnapshot: jsonb('execution_snapshot').notNull(),
pausePoints: jsonb('pause_points').notNull(),
totalPauseCount: integer('total_pause_count').notNull(),
resumedCount: integer('resumed_count').notNull().default(0),
status: text('status').notNull().default('paused'),
metadata: jsonb('metadata').notNull().default(sql`'{}'::jsonb`),
pausedAt: timestamp('paused_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
expiresAt: timestamp('expires_at'),
},
(table) => ({
workflowIdx: index('paused_executions_workflow_id_idx').on(table.workflowId),
statusIdx: index('paused_executions_status_idx').on(table.status),
executionUnique: uniqueIndex('paused_executions_execution_id_unique').on(table.executionId),
})
)
export const resumeQueue = pgTable(
'resume_queue',
{
id: text('id').primaryKey(),
pausedExecutionId: text('paused_execution_id')
.notNull()
.references(() => pausedExecutions.id, { onDelete: 'cascade' }),
parentExecutionId: text('parent_execution_id').notNull(),
newExecutionId: text('new_execution_id').notNull(),
contextId: text('context_id').notNull(),
resumeInput: jsonb('resume_input'),
status: text('status').notNull().default('pending'),
queuedAt: timestamp('queued_at').notNull().defaultNow(),
claimedAt: timestamp('claimed_at'),
completedAt: timestamp('completed_at'),
failureReason: text('failure_reason'),
},
(table) => ({
parentStatusIdx: index('resume_queue_parent_status_idx').on(
table.parentExecutionId,
table.status,
table.queuedAt
),
newExecutionIdx: index('resume_queue_new_execution_idx').on(table.newExecutionId),
})
)
export const environment = pgTable('environment', {
id: text('id').primaryKey(), // Use the user id as the key
userId: text('user_id')