From cf4de22c9238944495b502a3996c0af60a52e39a Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Fri, 16 Oct 2026 14:08:22 +0200 Subject: [PATCH] feat(core): Add persistence for Agent plan stops and job ownership (no-changelog) --- docs/generated/postgres-schema/README.md | 10 +- .../public.agent_background_job.md | 47 ++++++ .../postgres-schema/public.agent_execution.md | 29 +++- .../public.agent_execution_threads.md | 2 + .../postgres-schema/public.agent_history.md | 1 + .../public.agent_message_queue.md | 1 + .../postgres-schema/public.agent_plan.md | 29 +++- .../public.agent_thread_grants.md | 1 + .../postgres-schema/public.agents.md | 4 + .../postgres-schema/public.project.md | 1 + docs/generated/postgres-schema/public.user.md | 1 + docs/generated/sqlite-schema/README.md | 10 +- .../sqlite-schema/agent_background_job.md | 51 ++++++- .../sqlite-schema/agent_execution.md | 29 +++- .../sqlite-schema/agent_execution_threads.md | 4 +- docs/generated/sqlite-schema/agent_history.md | 1 + .../sqlite-schema/agent_message_queue.md | 1 + docs/generated/sqlite-schema/agent_plan.md | 29 +++- .../sqlite-schema/agent_thread_grants.md | 1 + docs/generated/sqlite-schema/agents.md | 4 + docs/generated/sqlite-schema/project.md | 1 + docs/generated/sqlite-schema/user.md | 1 + .../api-types/src/agents/background-job.ts | 3 + packages/@n8n/api-types/src/agents/dto.ts | 4 + packages/@n8n/api-types/src/agents/index.ts | 2 + .../api-types/src/agents/task-cancellation.ts | 59 ++++++++ ...-AddSourceExecutionToAgentBackgroundJob.ts | 28 ++++ ...9437-AddTaskStopToAgentExecutionThreads.ts | 28 ++++ ...3059654-AddPlanTaskToAgentBackgroundJob.ts | 31 ++++ .../entities/agent-background-job.entity.ts | 28 +++- .../entities/agent-execution-thread.entity.ts | 6 +- .../modules/agents/types/agent-task-stop.ts | 28 ++++ ...-execution-to-agent-background-job.test.ts | 128 +++++++++++++++++ ...sk-stop-to-agent-execution-threads.test.ts | 122 ++++++++++++++++ ...-plan-task-to-agent-background-job.test.ts | 134 ++++++++++++++++++ 35 files changed, 846 insertions(+), 13 deletions(-) create mode 100644 packages/@n8n/api-types/src/agents/task-cancellation.ts create mode 100644 packages/@n8n/db/src/migrations/common/1792083059203-AddSourceExecutionToAgentBackgroundJob.ts create mode 100644 packages/@n8n/db/src/migrations/common/1792083059437-AddTaskStopToAgentExecutionThreads.ts create mode 100644 packages/@n8n/db/src/migrations/common/1792083059654-AddPlanTaskToAgentBackgroundJob.ts create mode 100644 packages/cli/src/modules/agents/types/agent-task-stop.ts create mode 100644 packages/cli/test/migration/1792083059203-add-source-execution-to-agent-background-job.test.ts create mode 100644 packages/cli/test/migration/1792083059437-add-task-stop-to-agent-execution-threads.test.ts create mode 100644 packages/cli/test/migration/1792083059654-add-plan-task-to-agent-background-job.test.ts diff --git a/docs/generated/postgres-schema/README.md b/docs/generated/postgres-schema/README.md index 8f6c07493fb..ebd69015851 100644 --- a/docs/generated/postgres-schema/README.md +++ b/docs/generated/postgres-schema/README.md @@ -9,7 +9,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand. | Name | Columns | Comment | Type | | ---- | ------- | ------- | ---- | | [public.activity_event](public.activity_event.md) | 11 | | BASE TABLE | -| [public.agent_background_job](public.agent_background_job.md) | 20 | | BASE TABLE | +| [public.agent_background_job](public.agent_background_job.md) | 23 | | BASE TABLE | | [public.agent_budget_applied_call](public.agent_budget_applied_call.md) | 2 | | BASE TABLE | | [public.agent_budget_spend](public.agent_budget_spend.md) | 4 | | BASE TABLE | | [public.agent_channel_status](public.agent_channel_status.md) | 11 | | BASE TABLE | @@ -23,7 +23,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand. | [public.agent_eval_run](public.agent_eval_run.md) | 14 | | BASE TABLE | | [public.agent_execution](public.agent_execution.md) | 23 | | BASE TABLE | | [public.agent_execution_message_links](public.agent_execution_message_links.md) | 5 | | BASE TABLE | -| [public.agent_execution_threads](public.agent_execution_threads.md) | 19 | | BASE TABLE | +| [public.agent_execution_threads](public.agent_execution_threads.md) | 20 | | BASE TABLE | | [public.agent_files](public.agent_files.md) | 10 | | BASE TABLE | | [public.agent_history](public.agent_history.md) | 9 | | BASE TABLE | | [public.agent_message_queue](public.agent_message_queue.md) | 10 | | BASE TABLE | @@ -198,6 +198,8 @@ erDiagram "public.activity_event" }o--o| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL" "public.activity_event" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" "public.agent_background_job" }o--|| "public.agents" : "FOREIGN KEY (#quot;parentAgentId#quot;) REFERENCES agents(id) ON DELETE CASCADE" +"public.agent_background_job" }o--o| "public.agent_execution" : "FOREIGN KEY (#quot;sourceExecutionId#quot;) REFERENCES agent_execution(id) ON DELETE SET NULL" +"public.agent_background_job" }o--o| "public.agent_plan" : "FOREIGN KEY (#quot;planId#quot;) REFERENCES agent_plan(id) ON DELETE SET NULL" "public.agent_channel_status" }o--|| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE" "public.agent_chat_attachments" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" "public.agent_chat_attachments" }o--o| "public.agents" : "FOREIGN KEY (#quot;agentId#quot;) REFERENCES agents(id) ON DELETE CASCADE" @@ -466,8 +468,11 @@ erDiagram varchar_255_ parentResourceId varchar_128_ parentThreadId uuid pauseRequestId + uuid planId FK + uuid planTaskId text result timestamp_3__with_time_zone settledAt + varchar_36_ sourceExecutionId FK varchar_16_ status varchar_36_ subAgentId timestamp_3__with_time_zone timeoutAt @@ -635,6 +640,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.agent_background_job.md b/docs/generated/postgres-schema/public.agent_background_job.md index ef567001ba6..f0df0dd79e0 100644 --- a/docs/generated/postgres-schema/public.agent_background_job.md +++ b/docs/generated/postgres-schema/public.agent_background_job.md @@ -16,8 +16,11 @@ | parentResourceId | varchar(255) | | false | | | Memory resource of the parent agent run | | parentThreadId | varchar(128) | | false | | | | | pauseRequestId | uuid | | true | | | Groups jobs stopped by one user request | +| planId | uuid | | true | | [public.agent_plan](public.agent_plan.md) | Plan that owns this work | +| planTaskId | uuid | | true | | | Task ID in the saved plan document | | result | text | | true | | | Final answer of a settled sub-agent job | | settledAt | timestamp(3) with time zone | | true | | | | +| sourceExecutionId | varchar(36) | | true | | [public.agent_execution](public.agent_execution.md) | Execution that dispatched this job | | status | varchar(16) | | false | | | running: child works; suspended: child awaits approval; paused: user stopped the child; completed, failed, cancelled: terminal | | subAgentId | varchar(36) | | true | | | Sub-agent jobs only | | timeoutAt | timestamp(3) with time zone | | true | | | When reconciliation fails the job as timed out; NULL means no timeout | @@ -31,6 +34,8 @@ | ---- | ---- | ---------- | | CHK_agent_background_job_kind | CHECK | CHECK (((kind)::text = ANY ((ARRAY['subagent'::character varying, 'workflow'::character varying])::text[]))) | | CHK_agent_background_job_status | CHECK | CHECK (((status)::text = ANY ((ARRAY['running'::character varying, 'suspended'::character varying, 'paused'::character varying, 'completed'::character varying, 'failed'::character varying, 'cancelled'::character varying])::text[]))) | +| FK_4db3b7ebea4ed4f69649982018c | FOREIGN KEY | FOREIGN KEY ("sourceExecutionId") REFERENCES agent_execution(id) ON DELETE SET NULL | +| FK_ce131bff629d89015893e350e9c | FOREIGN KEY | FOREIGN KEY ("planId") REFERENCES agent_plan(id) ON DELETE SET NULL | | FK_d46c6f00730c2ef8bcb6ee24b67 | FOREIGN KEY | FOREIGN KEY ("parentAgentId") REFERENCES agents(id) ON DELETE CASCADE | | PK_6e0db58281aa2b4c956dc0d58e9 | PRIMARY KEY | PRIMARY KEY (id) | | agent_background_job_createdAt_not_null | n | NOT NULL "createdAt" | @@ -51,6 +56,8 @@ | IDX_93d62baabe9858816b5adafb44 | CREATE INDEX "IDX_93d62baabe9858816b5adafb44" ON public.agent_background_job USING btree ("parentThreadId", status) | | IDX_agent_background_job_childExecutionId | CREATE UNIQUE INDEX "IDX_agent_background_job_childExecutionId" ON public.agent_background_job USING btree ("childExecutionId") WHERE ("childExecutionId" IS NOT NULL) | | IDX_agent_background_job_parentThreadId | CREATE INDEX "IDX_agent_background_job_parentThreadId" ON public.agent_background_job USING btree ("parentThreadId") WHERE (((status)::text <> 'running'::text) AND ("notifiedAt" IS NULL)) | +| IDX_agent_background_job_planId_planTaskId | CREATE INDEX "IDX_agent_background_job_planId_planTaskId" ON public.agent_background_job USING btree ("planId", "planTaskId") | +| IDX_agent_background_job_sourceExecutionId | CREATE INDEX "IDX_agent_background_job_sourceExecutionId" ON public.agent_background_job USING btree ("sourceExecutionId") | | IDX_agent_background_job_timeoutAt | CREATE INDEX "IDX_agent_background_job_timeoutAt" ON public.agent_background_job USING btree ("timeoutAt") WHERE ((status)::text = ANY ((ARRAY['running'::character varying, 'suspended'::character varying])::text[])) | | IDX_d46c6f00730c2ef8bcb6ee24b6 | CREATE INDEX "IDX_d46c6f00730c2ef8bcb6ee24b6" ON public.agent_background_job USING btree ("parentAgentId") | | IDX_e43e630272995a93dfeb94ab3e | CREATE INDEX "IDX_e43e630272995a93dfeb94ab3e" ON public.agent_background_job USING btree ("settledAt") | @@ -62,6 +69,8 @@ erDiagram "public.agent_background_job" }o--|| "public.agents" : "FOREIGN KEY (#quot;parentAgentId#quot;) REFERENCES agents(id) ON DELETE CASCADE" +"public.agent_background_job" }o--o| "public.agent_plan" : "FOREIGN KEY (#quot;planId#quot;) REFERENCES agent_plan(id) ON DELETE SET NULL" +"public.agent_background_job" }o--o| "public.agent_execution" : "FOREIGN KEY (#quot;sourceExecutionId#quot;) REFERENCES agent_execution(id) ON DELETE SET NULL" "public.agent_background_job" { varchar_36_ childExecutionId @@ -76,8 +85,11 @@ erDiagram varchar_255_ parentResourceId varchar_128_ parentThreadId uuid pauseRequestId + uuid planId FK + uuid planTaskId text result timestamp_3__with_time_zone settledAt + varchar_36_ sourceExecutionId FK varchar_16_ status varchar_36_ subAgentId timestamp_3__with_time_zone timeoutAt @@ -101,6 +113,41 @@ erDiagram timestamp_3__with_time_zone updatedAt varchar_36_ versionId } +"public.agent_plan" { + timestamp_3__with_time_zone closedAt + timestamp_3__with_time_zone createdAt + json data + integer formatVersion + uuid id + integer revision + varchar_128_ threadId FK + timestamp_3__with_time_zone updatedAt +} +"public.agent_execution" { + boolean acceptsSteering + json attachments + json author + integer completionTokens + double_precision cost + timestamp_3__with_time_zone createdAt + integer duration + text error + json failureSummary + varchar_16_ hitlStatus + varchar_36_ id + varchar_255_ model + integer promptTokens + varchar_32_ source + timestamp_3__with_time_zone startedAt + varchar_16_ status + timestamp_3__with_time_zone stoppedAt + varchar_2_ storedAt + varchar_128_ threadId FK + json timeline + integer totalTokens + timestamp_3__with_time_zone updatedAt + text userMessage +} ``` --- diff --git a/docs/generated/postgres-schema/public.agent_execution.md b/docs/generated/postgres-schema/public.agent_execution.md index 7094f46732d..594ae320762 100644 --- a/docs/generated/postgres-schema/public.agent_execution.md +++ b/docs/generated/postgres-schema/public.agent_execution.md @@ -14,7 +14,7 @@ | error | text | | true | | | | | failureSummary | json | | true | | | Execution failure projection as {count, latest} for session list queries | | hitlStatus | varchar(16) | | true | | | | -| id | varchar(36) | | false | [public.agent_execution_message_links](public.agent_execution_message_links.md) [public.agent_message_queue](public.agent_message_queue.md) | | | +| id | varchar(36) | | false | [public.agent_background_job](public.agent_background_job.md) [public.agent_execution_message_links](public.agent_execution_message_links.md) [public.agent_message_queue](public.agent_message_queue.md) | | | | model | varchar(255) | | true | | | | | promptTokens | integer | | true | | | | | source | varchar(32) | | true | | | | @@ -59,6 +59,7 @@ ```mermaid erDiagram +"public.agent_background_job" }o--o| "public.agent_execution" : "FOREIGN KEY (#quot;sourceExecutionId#quot;) REFERENCES agent_execution(id) ON DELETE SET NULL" "public.agent_execution_message_links" }o--|| "public.agent_execution" : "FOREIGN KEY (#quot;executionId#quot;) REFERENCES agent_execution(id) ON DELETE CASCADE" "public.agent_message_queue" }o--o| "public.agent_execution" : "FOREIGN KEY (#quot;executionId#quot;) REFERENCES agent_execution(id)" "public.agent_message_queue" }o--o| "public.agent_execution" : "FOREIGN KEY (#quot;steeringExecutionId#quot;) REFERENCES agent_execution(id)" @@ -89,6 +90,31 @@ erDiagram timestamp_3__with_time_zone updatedAt text userMessage } +"public.agent_background_job" { + varchar_36_ childExecutionId + varchar_128_ childThreadId + timestamp_3__with_time_zone createdAt + text error + varchar_36_ id + varchar_16_ kind + timestamp_3__with_time_zone notifiedAt + varchar_36_ parentAgentId FK + varchar_64_ parentPrincipalHash + varchar_255_ parentResourceId + varchar_128_ parentThreadId + uuid pauseRequestId + uuid planId FK + uuid planTaskId + text result + timestamp_3__with_time_zone settledAt + varchar_36_ sourceExecutionId FK + varchar_16_ status + varchar_36_ subAgentId + timestamp_3__with_time_zone timeoutAt + varchar_255_ title + timestamp_3__with_time_zone updatedAt + varchar_36_ workflowId +} "public.agent_execution_message_links" { timestamp_3__with_time_zone createdAt varchar_6_ direction @@ -121,6 +147,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.agent_execution_threads.md b/docs/generated/postgres-schema/public.agent_execution_threads.md index 07f0bc8ef83..b31d6534714 100644 --- a/docs/generated/postgres-schema/public.agent_execution_threads.md +++ b/docs/generated/postgres-schema/public.agent_execution_threads.md @@ -16,6 +16,7 @@ | projectId | varchar(255) | | false | | [public.project](public.project.md) | | | sessionNumber | integer | 0 | false | | | | | taskId | varchar(32) | | true | | | Published task ID that triggered this session; not an FK because published runs can outlive draft task definition rows | +| taskStop | json | | true | | | Latest task stop boundary and unresolved stop errors | | taskVersionId | varchar(36) | | true | | [public.agent_history](public.agent_history.md) | Published agent_history version that supplied the task snapshot | | title | varchar(255) | | true | | | | | totalCompletionTokens | integer | 0 | false | | | | @@ -84,6 +85,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.agent_history.md b/docs/generated/postgres-schema/public.agent_history.md index f88d49c9688..3f7090da027 100644 --- a/docs/generated/postgres-schema/public.agent_history.md +++ b/docs/generated/postgres-schema/public.agent_history.md @@ -103,6 +103,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.agent_message_queue.md b/docs/generated/postgres-schema/public.agent_message_queue.md index a2d4b104d97..ec316b58998 100644 --- a/docs/generated/postgres-schema/public.agent_message_queue.md +++ b/docs/generated/postgres-schema/public.agent_message_queue.md @@ -118,6 +118,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.agent_plan.md b/docs/generated/postgres-schema/public.agent_plan.md index 91b7c0f4be0..426bd998699 100644 --- a/docs/generated/postgres-schema/public.agent_plan.md +++ b/docs/generated/postgres-schema/public.agent_plan.md @@ -8,7 +8,7 @@ | createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | | | data | json | | false | | | Opaque plan document | | formatVersion | integer | | false | | | Version of the plan JSON format | -| id | uuid | | false | [public.agent_plan_history](public.agent_plan_history.md) | | Plan ID generated by the caller | +| id | uuid | | false | [public.agent_background_job](public.agent_background_job.md) [public.agent_plan_history](public.agent_plan_history.md) | | Plan ID generated by the caller | | revision | integer | 1 | false | | | Current document revision | | threadId | varchar(128) | | false | | [public.agent_execution_threads](public.agent_execution_threads.md) | Session that owns the plan | | updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | | @@ -42,6 +42,7 @@ ```mermaid erDiagram +"public.agent_background_job" }o--o| "public.agent_plan" : "FOREIGN KEY (#quot;planId#quot;) REFERENCES agent_plan(id) ON DELETE SET NULL" "public.agent_plan_history" }o--|| "public.agent_plan" : "FOREIGN KEY (#quot;planId#quot;) REFERENCES agent_plan(id) ON DELETE CASCADE" "public.agent_plan" }o--|| "public.agent_execution_threads" : "FOREIGN KEY (#quot;threadId#quot;) REFERENCES agent_execution_threads(id) ON DELETE CASCADE" @@ -55,6 +56,31 @@ erDiagram varchar_128_ threadId FK timestamp_3__with_time_zone updatedAt } +"public.agent_background_job" { + varchar_36_ childExecutionId + varchar_128_ childThreadId + timestamp_3__with_time_zone createdAt + text error + varchar_36_ id + varchar_16_ kind + timestamp_3__with_time_zone notifiedAt + varchar_36_ parentAgentId FK + varchar_64_ parentPrincipalHash + varchar_255_ parentResourceId + varchar_128_ parentThreadId + uuid pauseRequestId + uuid planId FK + uuid planTaskId + text result + timestamp_3__with_time_zone settledAt + varchar_36_ sourceExecutionId FK + varchar_16_ status + varchar_36_ subAgentId + timestamp_3__with_time_zone timeoutAt + varchar_255_ title + timestamp_3__with_time_zone updatedAt + varchar_36_ workflowId +} "public.agent_plan_history" { timestamp_3__with_time_zone closedAt timestamp_3__with_time_zone createdAt @@ -76,6 +102,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.agent_thread_grants.md b/docs/generated/postgres-schema/public.agent_thread_grants.md index 2bcafbbd9e7..83602d84eb7 100644 --- a/docs/generated/postgres-schema/public.agent_thread_grants.md +++ b/docs/generated/postgres-schema/public.agent_thread_grants.md @@ -52,6 +52,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.agents.md b/docs/generated/postgres-schema/public.agents.md index 63aa85de828..858951ea103 100644 --- a/docs/generated/postgres-schema/public.agents.md +++ b/docs/generated/postgres-schema/public.agents.md @@ -115,8 +115,11 @@ erDiagram varchar_255_ parentResourceId varchar_128_ parentThreadId uuid pauseRequestId + uuid planId FK + uuid planTaskId text result timestamp_3__with_time_zone settledAt + varchar_36_ sourceExecutionId FK varchar_16_ status varchar_36_ subAgentId timestamp_3__with_time_zone timeoutAt @@ -198,6 +201,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.project.md b/docs/generated/postgres-schema/public.project.md index 04d30b4ff68..6b70d75a206 100644 --- a/docs/generated/postgres-schema/public.project.md +++ b/docs/generated/postgres-schema/public.project.md @@ -130,6 +130,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/postgres-schema/public.user.md b/docs/generated/postgres-schema/public.user.md index 8db545537e7..d6a426d4652 100644 --- a/docs/generated/postgres-schema/public.user.md +++ b/docs/generated/postgres-schema/public.user.md @@ -175,6 +175,7 @@ erDiagram varchar_255_ projectId FK integer sessionNumber varchar_32_ taskId + json taskStop varchar_36_ taskVersionId FK varchar_255_ title integer totalCompletionTokens diff --git a/docs/generated/sqlite-schema/README.md b/docs/generated/sqlite-schema/README.md index 6e946dc580d..742e6a74133 100644 --- a/docs/generated/sqlite-schema/README.md +++ b/docs/generated/sqlite-schema/README.md @@ -9,7 +9,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand. | Name | Columns | Comment | Type | | ---- | ------- | ------- | ---- | | [activity_event](activity_event.md) | 11 | | table | -| [agent_background_job](agent_background_job.md) | 20 | | table | +| [agent_background_job](agent_background_job.md) | 23 | | table | | [agent_budget_applied_call](agent_budget_applied_call.md) | 2 | | table | | [agent_budget_spend](agent_budget_spend.md) | 4 | | table | | [agent_channel_status](agent_channel_status.md) | 11 | | table | @@ -23,7 +23,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand. | [agent_eval_run](agent_eval_run.md) | 14 | | table | | [agent_execution](agent_execution.md) | 23 | | table | | [agent_execution_message_links](agent_execution_message_links.md) | 5 | | table | -| [agent_execution_threads](agent_execution_threads.md) | 19 | | table | +| [agent_execution_threads](agent_execution_threads.md) | 20 | | table | | [agent_files](agent_files.md) | 10 | | table | | [agent_history](agent_history.md) | 9 | | table | | [agent_message_queue](agent_message_queue.md) | 10 | | table | @@ -179,6 +179,8 @@ erDiagram "activity_event" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "activity_event" }o--o| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" +"agent_background_job" }o--o| "agent_plan" : "FOREIGN KEY (planId) REFERENCES agent_plan (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" +"agent_background_job" }o--o| "agent_execution" : "FOREIGN KEY (sourceExecutionId) REFERENCES agent_execution (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" "agent_background_job" }o--|| "agents" : "FOREIGN KEY (parentAgentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "agent_channel_status" |o--|| "agents" : "FOREIGN KEY (agentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "agent_chat_attachments" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" @@ -452,8 +454,11 @@ erDiagram varchar_255_ parentResourceId varchar_128_ parentThreadId varchar pauseRequestId + varchar planId FK + varchar planTaskId TEXT result datetime_3_ settledAt + varchar_36_ sourceExecutionId FK varchar_16_ status varchar_36_ subAgentId datetime_3_ timeoutAt @@ -621,6 +626,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/agent_background_job.md b/docs/generated/sqlite-schema/agent_background_job.md index f7e22cd9e97..b5e59ef8cf4 100644 --- a/docs/generated/sqlite-schema/agent_background_job.md +++ b/docs/generated/sqlite-schema/agent_background_job.md @@ -6,7 +6,7 @@ Table Definition ```sql -CREATE TABLE "agent_background_job" ("id" varchar(36) PRIMARY KEY NOT NULL, "kind" varchar(16) NOT NULL, "status" varchar(16) NOT NULL, "parentAgentId" varchar(36) NOT NULL, "parentThreadId" varchar(128) NOT NULL, "title" varchar(255) NOT NULL, "subAgentId" varchar(36), "childThreadId" varchar(128), "childExecutionId" varchar(36), "workflowId" varchar(36), "timeoutAt" datetime(3), "result" text, "error" text, "settledAt" datetime(3), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "notifiedAt" datetime(3), "parentResourceId" varchar(255) NOT NULL, "parentPrincipalHash" varchar(64) NOT NULL, "pauseRequestId" varchar, CONSTRAINT "CHK_agent_background_job_kind" CHECK (((("kind" IN ('subagent', 'workflow'))))), CONSTRAINT "CHK_agent_background_job_status" CHECK ("status" IN ('running', 'suspended', 'paused', 'completed', 'failed', 'cancelled')), CONSTRAINT "FK_d46c6f00730c2ef8bcb6ee24b67" FOREIGN KEY ("parentAgentId") REFERENCES "agents" ("id") ON DELETE CASCADE ON UPDATE NO ACTION) +CREATE TABLE "agent_background_job" ("id" varchar(36) PRIMARY KEY NOT NULL, "kind" varchar(16) NOT NULL, "status" varchar(16) NOT NULL, "parentAgentId" varchar(36) NOT NULL, "parentThreadId" varchar(128) NOT NULL, "title" varchar(255) NOT NULL, "subAgentId" varchar(36), "childThreadId" varchar(128), "childExecutionId" varchar(36), "workflowId" varchar(36), "timeoutAt" datetime(3), "result" text, "error" text, "settledAt" datetime(3), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "notifiedAt" datetime(3), "parentResourceId" varchar(255) NOT NULL, "parentPrincipalHash" varchar(64) NOT NULL, "pauseRequestId" varchar, "sourceExecutionId" varchar(36), "planId" varchar, "planTaskId" varchar, CONSTRAINT "CHK_agent_background_job_kind" CHECK (((("kind" IN ('subagent', 'workflow'))))), CONSTRAINT "CHK_agent_background_job_status" CHECK ("status" IN ('running', 'suspended', 'paused', 'completed', 'failed', 'cancelled')), CONSTRAINT "FK_d46c6f00730c2ef8bcb6ee24b67" FOREIGN KEY ("parentAgentId") REFERENCES "agents" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_4db3b7ebea4ed4f69649982018c" FOREIGN KEY ("sourceExecutionId") REFERENCES "agent_execution" ("id") ON DELETE SET NULL, CONSTRAINT "FK_ce131bff629d89015893e350e9c" FOREIGN KEY ("planId") REFERENCES "agent_plan" ("id") ON DELETE SET NULL) ``` @@ -27,8 +27,11 @@ CREATE TABLE "agent_background_job" ("id" varchar(36) PRIMARY KEY NOT NULL, "kin | parentResourceId | varchar(255) | | false | | | | | parentThreadId | varchar(128) | | false | | | | | pauseRequestId | varchar | | true | | | | +| planId | varchar | | true | | [agent_plan](agent_plan.md) | | +| planTaskId | varchar | | true | | | | | result | TEXT | | true | | | | | settledAt | datetime(3) | | true | | | | +| sourceExecutionId | varchar(36) | | true | | [agent_execution](agent_execution.md) | | | status | varchar(16) | | false | | | | | subAgentId | varchar(36) | | true | | | | | timeoutAt | datetime(3) | | true | | | | @@ -42,7 +45,9 @@ CREATE TABLE "agent_background_job" ("id" varchar(36) PRIMARY KEY NOT NULL, "kin | ---- | ---- | ---------- | | - | CHECK | CHECK (((("kind" IN ('subagent', 'workflow'))))) | | - | CHECK | CHECK ("status" IN ('running', 'suspended', 'paused', 'completed', 'failed', 'cancelled')) | -| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (parentAgentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE | +| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (planId) REFERENCES agent_plan (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE | +| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (sourceExecutionId) REFERENCES agent_execution (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE | +| - (Foreign key ID: 2) | FOREIGN KEY | FOREIGN KEY (parentAgentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE | | id | PRIMARY KEY | PRIMARY KEY (id) | | sqlite_autoindex_agent_background_job_1 | PRIMARY KEY | PRIMARY KEY (id) | @@ -53,6 +58,8 @@ CREATE TABLE "agent_background_job" ("id" varchar(36) PRIMARY KEY NOT NULL, "kin | IDX_93d62baabe9858816b5adafb44 | CREATE INDEX "IDX_93d62baabe9858816b5adafb44" ON "agent_background_job" ("parentThreadId", "status") | | IDX_agent_background_job_childExecutionId | CREATE UNIQUE INDEX "IDX_agent_background_job_childExecutionId" ON "agent_background_job" ("childExecutionId") WHERE "childExecutionId" IS NOT NULL | | IDX_agent_background_job_parentThreadId | CREATE INDEX "IDX_agent_background_job_parentThreadId" ON "agent_background_job" ("parentThreadId") WHERE "status" <> 'running' AND "notifiedAt" IS NULL | +| IDX_agent_background_job_planId_planTaskId | CREATE INDEX "IDX_agent_background_job_planId_planTaskId" ON "agent_background_job" ("planId", "planTaskId") | +| IDX_agent_background_job_sourceExecutionId | CREATE INDEX "IDX_agent_background_job_sourceExecutionId" ON "agent_background_job" ("sourceExecutionId") | | IDX_agent_background_job_timeoutAt | CREATE INDEX "IDX_agent_background_job_timeoutAt" ON "agent_background_job" ("timeoutAt") WHERE "status" IN ('running', 'suspended') | | IDX_d46c6f00730c2ef8bcb6ee24b6 | CREATE INDEX "IDX_d46c6f00730c2ef8bcb6ee24b6" ON "agent_background_job" ("parentAgentId") | | IDX_e43e630272995a93dfeb94ab3e | CREATE INDEX "IDX_e43e630272995a93dfeb94ab3e" ON "agent_background_job" ("settledAt") | @@ -64,6 +71,8 @@ CREATE TABLE "agent_background_job" ("id" varchar(36) PRIMARY KEY NOT NULL, "kin erDiagram "agent_background_job" }o--|| "agents" : "FOREIGN KEY (parentAgentId) REFERENCES agents (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" +"agent_background_job" }o--o| "agent_plan" : "FOREIGN KEY (planId) REFERENCES agent_plan (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" +"agent_background_job" }o--o| "agent_execution" : "FOREIGN KEY (sourceExecutionId) REFERENCES agent_execution (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" "agent_background_job" { varchar_36_ childExecutionId @@ -78,8 +87,11 @@ erDiagram varchar_255_ parentResourceId varchar_128_ parentThreadId varchar pauseRequestId + varchar planId FK + varchar planTaskId TEXT result datetime_3_ settledAt + varchar_36_ sourceExecutionId FK varchar_16_ status varchar_36_ subAgentId datetime_3_ timeoutAt @@ -103,6 +115,41 @@ erDiagram datetime_3_ updatedAt varchar_36_ versionId } +"agent_plan" { + datetime_3_ closedAt + datetime_3_ createdAt + TEXT data + INTEGER formatVersion + varchar id PK + INTEGER revision + varchar_128_ threadId FK + datetime_3_ updatedAt +} +"agent_execution" { + BOOLEAN acceptsSteering + TEXT attachments + TEXT author + INTEGER completionTokens + REAL cost + datetime_3_ createdAt + INTEGER duration + TEXT error + TEXT failureSummary + varchar_16_ hitlStatus + varchar_36_ id PK + varchar_255_ model + INTEGER promptTokens + varchar_32_ source + datetime_3_ startedAt + varchar_16_ status + datetime_3_ stoppedAt + varchar_2_ storedAt + varchar_128_ threadId FK + TEXT timeline + INTEGER totalTokens + datetime_3_ updatedAt + TEXT userMessage +} ``` --- diff --git a/docs/generated/sqlite-schema/agent_execution.md b/docs/generated/sqlite-schema/agent_execution.md index 3f5e86bd2a2..11f9f28f5e0 100644 --- a/docs/generated/sqlite-schema/agent_execution.md +++ b/docs/generated/sqlite-schema/agent_execution.md @@ -25,7 +25,7 @@ CREATE TABLE "agent_execution" ("id" varchar(36) PRIMARY KEY NOT NULL, "threadId | error | TEXT | | true | | | | | failureSummary | TEXT | | true | | | | | hitlStatus | varchar(16) | | true | | | | -| id | varchar(36) | | false | [agent_execution_message_links](agent_execution_message_links.md) [agent_message_queue](agent_message_queue.md) | | | +| id | varchar(36) | | false | [agent_background_job](agent_background_job.md) [agent_execution_message_links](agent_execution_message_links.md) [agent_message_queue](agent_message_queue.md) | | | | model | varchar(255) | | true | | | | | promptTokens | INTEGER | | true | | | | | source | varchar(32) | | true | | | | @@ -63,6 +63,7 @@ CREATE TABLE "agent_execution" ("id" varchar(36) PRIMARY KEY NOT NULL, "threadId ```mermaid erDiagram +"agent_background_job" }o--o| "agent_execution" : "FOREIGN KEY (sourceExecutionId) REFERENCES agent_execution (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" "agent_execution_message_links" |o--|| "agent_execution" : "FOREIGN KEY (executionId) REFERENCES agent_execution (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "agent_message_queue" }o--o| "agent_execution" : "FOREIGN KEY (executionId) REFERENCES agent_execution (id) ON UPDATE NO ACTION ON DELETE NO ACTION MATCH NONE" "agent_message_queue" }o--o| "agent_execution" : "FOREIGN KEY (steeringExecutionId) REFERENCES agent_execution (id) ON UPDATE NO ACTION ON DELETE NO ACTION MATCH NONE" @@ -93,6 +94,31 @@ erDiagram datetime_3_ updatedAt TEXT userMessage } +"agent_background_job" { + varchar_36_ childExecutionId + varchar_128_ childThreadId + datetime_3_ createdAt + TEXT error + varchar_36_ id PK + varchar_16_ kind + datetime_3_ notifiedAt + varchar_36_ parentAgentId FK + varchar_64_ parentPrincipalHash + varchar_255_ parentResourceId + varchar_128_ parentThreadId + varchar pauseRequestId + varchar planId FK + varchar planTaskId + TEXT result + datetime_3_ settledAt + varchar_36_ sourceExecutionId FK + varchar_16_ status + varchar_36_ subAgentId + datetime_3_ timeoutAt + varchar_255_ title + datetime_3_ updatedAt + varchar_36_ workflowId +} "agent_execution_message_links" { datetime_3_ createdAt varchar_6_ direction @@ -125,6 +151,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/agent_execution_threads.md b/docs/generated/sqlite-schema/agent_execution_threads.md index db855a5098d..afe047e9748 100644 --- a/docs/generated/sqlite-schema/agent_execution_threads.md +++ b/docs/generated/sqlite-schema/agent_execution_threads.md @@ -6,7 +6,7 @@ Table Definition ```sql -CREATE TABLE "agent_execution_threads" ("id" varchar(128) PRIMARY KEY NOT NULL, "agentId" varchar(36) NOT NULL, "agentName" varchar(255) NOT NULL, "projectId" varchar(255) NOT NULL, "sessionNumber" integer NOT NULL DEFAULT (0), "totalPromptTokens" integer NOT NULL DEFAULT (0), "totalCompletionTokens" integer NOT NULL DEFAULT (0), "totalCost" real NOT NULL DEFAULT (0), "totalDuration" integer NOT NULL DEFAULT (0), "title" varchar(255), "emoji" varchar(8), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "taskId" varchar(32), "taskVersionId" varchar(36), "parentThreadId" varchar(128), "parentAgentId" varchar(36), "ownerId" varchar, "accessScope" varchar(16) NOT NULL DEFAULT ('user'), CONSTRAINT "CHK_agent_execution_threads_accessScope" CHECK ("accessScope" IN ('user', 'project')), CONSTRAINT "FK_f00b52d74fe11838e1fe086deea" FOREIGN KEY ("taskVersionId") REFERENCES "agent_history" ("versionId") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_0468a9dc35597314e641d4722aa" FOREIGN KEY ("agentId") REFERENCES "agents" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_0e2f8bf92a7a9c88b89670f701c" FOREIGN KEY ("projectId") REFERENCES "project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_agent_execution_threads_owner" FOREIGN KEY ("ownerId") REFERENCES "user" ("id") ON DELETE SET NULL) +CREATE TABLE "agent_execution_threads" ("id" varchar(128) PRIMARY KEY NOT NULL, "agentId" varchar(36) NOT NULL, "agentName" varchar(255) NOT NULL, "projectId" varchar(255) NOT NULL, "sessionNumber" integer NOT NULL DEFAULT (0), "totalPromptTokens" integer NOT NULL DEFAULT (0), "totalCompletionTokens" integer NOT NULL DEFAULT (0), "totalCost" real NOT NULL DEFAULT (0), "totalDuration" integer NOT NULL DEFAULT (0), "title" varchar(255), "emoji" varchar(8), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "taskId" varchar(32), "taskVersionId" varchar(36), "parentThreadId" varchar(128), "parentAgentId" varchar(36), "ownerId" varchar, "accessScope" varchar(16) NOT NULL DEFAULT ('user'), "taskStop" TEXT, CONSTRAINT "CHK_agent_execution_threads_accessScope" CHECK ("accessScope" IN ('user', 'project')), CONSTRAINT "FK_f00b52d74fe11838e1fe086deea" FOREIGN KEY ("taskVersionId") REFERENCES "agent_history" ("versionId") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_0468a9dc35597314e641d4722aa" FOREIGN KEY ("agentId") REFERENCES "agents" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_0e2f8bf92a7a9c88b89670f701c" FOREIGN KEY ("projectId") REFERENCES "project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_agent_execution_threads_owner" FOREIGN KEY ("ownerId") REFERENCES "user" ("id") ON DELETE SET NULL) ``` @@ -27,6 +27,7 @@ CREATE TABLE "agent_execution_threads" ("id" varchar(128) PRIMARY KEY NOT NULL, | projectId | varchar(255) | | false | | [project](project.md) | | | sessionNumber | INTEGER | 0 | false | | | | | taskId | varchar(32) | | true | | | | +| taskStop | TEXT | | true | | | | | taskVersionId | varchar(36) | | true | | [agent_history](agent_history.md) | | | title | varchar(255) | | true | | | | | totalCompletionTokens | INTEGER | 0 | false | | | | @@ -84,6 +85,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/agent_history.md b/docs/generated/sqlite-schema/agent_history.md index b157fab3042..a4114005394 100644 --- a/docs/generated/sqlite-schema/agent_history.md +++ b/docs/generated/sqlite-schema/agent_history.md @@ -110,6 +110,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/agent_message_queue.md b/docs/generated/sqlite-schema/agent_message_queue.md index 667af0c8609..31434931ce2 100644 --- a/docs/generated/sqlite-schema/agent_message_queue.md +++ b/docs/generated/sqlite-schema/agent_message_queue.md @@ -121,6 +121,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/agent_plan.md b/docs/generated/sqlite-schema/agent_plan.md index 1d296d0d846..a20a2d564bb 100644 --- a/docs/generated/sqlite-schema/agent_plan.md +++ b/docs/generated/sqlite-schema/agent_plan.md @@ -19,7 +19,7 @@ CREATE TABLE "agent_plan" ("id" varchar PRIMARY KEY NOT NULL, "threadId" varchar | createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | | | data | TEXT | | false | | | | | formatVersion | INTEGER | | false | | | | -| id | varchar | | false | [agent_plan_history](agent_plan_history.md) | | | +| id | varchar | | false | [agent_background_job](agent_background_job.md) [agent_plan_history](agent_plan_history.md) | | | | revision | INTEGER | 1 | false | | | | | threadId | varchar(128) | | false | | [agent_execution_threads](agent_execution_threads.md) | | | updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | | @@ -47,6 +47,7 @@ CREATE TABLE "agent_plan" ("id" varchar PRIMARY KEY NOT NULL, "threadId" varchar ```mermaid erDiagram +"agent_background_job" }o--o| "agent_plan" : "FOREIGN KEY (planId) REFERENCES agent_plan (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" "agent_plan_history" |o--|| "agent_plan" : "FOREIGN KEY (planId) REFERENCES agent_plan (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "agent_plan" }o--|| "agent_execution_threads" : "FOREIGN KEY (threadId) REFERENCES agent_execution_threads (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" @@ -60,6 +61,31 @@ erDiagram varchar_128_ threadId FK datetime_3_ updatedAt } +"agent_background_job" { + varchar_36_ childExecutionId + varchar_128_ childThreadId + datetime_3_ createdAt + TEXT error + varchar_36_ id PK + varchar_16_ kind + datetime_3_ notifiedAt + varchar_36_ parentAgentId FK + varchar_64_ parentPrincipalHash + varchar_255_ parentResourceId + varchar_128_ parentThreadId + varchar pauseRequestId + varchar planId FK + varchar planTaskId + TEXT result + datetime_3_ settledAt + varchar_36_ sourceExecutionId FK + varchar_16_ status + varchar_36_ subAgentId + datetime_3_ timeoutAt + varchar_255_ title + datetime_3_ updatedAt + varchar_36_ workflowId +} "agent_plan_history" { datetime_3_ closedAt datetime_3_ createdAt @@ -81,6 +107,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/agent_thread_grants.md b/docs/generated/sqlite-schema/agent_thread_grants.md index ebbc6fcdc37..288a6488f14 100644 --- a/docs/generated/sqlite-schema/agent_thread_grants.md +++ b/docs/generated/sqlite-schema/agent_thread_grants.md @@ -61,6 +61,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/agents.md b/docs/generated/sqlite-schema/agents.md index b7feda944a9..f8a77bc4e56 100644 --- a/docs/generated/sqlite-schema/agents.md +++ b/docs/generated/sqlite-schema/agents.md @@ -117,8 +117,11 @@ erDiagram varchar_255_ parentResourceId varchar_128_ parentThreadId varchar pauseRequestId + varchar planId FK + varchar planTaskId TEXT result datetime_3_ settledAt + varchar_36_ sourceExecutionId FK varchar_16_ status varchar_36_ subAgentId datetime_3_ timeoutAt @@ -200,6 +203,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/project.md b/docs/generated/sqlite-schema/project.md index 46f39c7fb0b..031389b6864 100644 --- a/docs/generated/sqlite-schema/project.md +++ b/docs/generated/sqlite-schema/project.md @@ -136,6 +136,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/docs/generated/sqlite-schema/user.md b/docs/generated/sqlite-schema/user.md index 1402b80f8f9..6d816e4a1fe 100644 --- a/docs/generated/sqlite-schema/user.md +++ b/docs/generated/sqlite-schema/user.md @@ -181,6 +181,7 @@ erDiagram varchar_255_ projectId FK INTEGER sessionNumber varchar_32_ taskId + TEXT taskStop varchar_36_ taskVersionId FK varchar_255_ title INTEGER totalCompletionTokens diff --git a/packages/@n8n/api-types/src/agents/background-job.ts b/packages/@n8n/api-types/src/agents/background-job.ts index 5c1152ccd1f..2b00b3ab1de 100644 --- a/packages/@n8n/api-types/src/agents/background-job.ts +++ b/packages/@n8n/api-types/src/agents/background-job.ts @@ -1,6 +1,9 @@ +import type { AgentPlanTaskRef } from './task-cancellation'; + import type { AgentBuilderOpenSuspension } from './types'; export interface AgentBackgroundJobDto { + planTask?: AgentPlanTaskRef; id: string; title: string; /** Source of the background job. */ diff --git a/packages/@n8n/api-types/src/agents/dto.ts b/packages/@n8n/api-types/src/agents/dto.ts index 58d4a8e8ca6..1646d5ec53f 100644 --- a/packages/@n8n/api-types/src/agents/dto.ts +++ b/packages/@n8n/api-types/src/agents/dto.ts @@ -457,3 +457,7 @@ export interface VectorStoreTestResult { export class AgentChatCancelDto extends Z.class({ scope: z.enum(['foreground', 'all']).optional(), }) {} + +export class AgentTaskCancellationDto extends Z.class({ + planId: z.string().uuid().toLowerCase().nullable(), +}) {} diff --git a/packages/@n8n/api-types/src/agents/index.ts b/packages/@n8n/api-types/src/agents/index.ts index f5e86f490bd..80b71847091 100644 --- a/packages/@n8n/api-types/src/agents/index.ts +++ b/packages/@n8n/api-types/src/agents/index.ts @@ -48,3 +48,5 @@ export { type CancellationResumeData, } from '../agent-builder-interactive'; export * from './agent-interaction.schema'; + +export type * from './task-cancellation'; diff --git a/packages/@n8n/api-types/src/agents/task-cancellation.ts b/packages/@n8n/api-types/src/agents/task-cancellation.ts new file mode 100644 index 00000000000..c8e7692a8a5 --- /dev/null +++ b/packages/@n8n/api-types/src/agents/task-cancellation.ts @@ -0,0 +1,59 @@ +export interface AgentPlanTaskRef { + planId: string; + planTaskId: string; +} + +/** Runtime state does not change the saved task statuses. */ +export interface AgentPlanWorkState { + stoppedTaskIds: string[]; + stoppingTaskIds: string[]; + fullyStopped: boolean; + stoppedAt: string | null; + taskStateAvailable: boolean; +} + +export interface AgentPlanItemDto { + id: string; + title: string; + description?: string; + resultSummary?: string; + status: 'pending' | 'in_progress' | 'done' | 'failed' | 'cancelled'; + dependsOn: string[]; +} + +export interface AgentPlanSnapshotDto { + workState?: AgentPlanWorkState; + readiness: { ready: string[]; blocked: Array<{ id: string; blockedBy: string[] }> }; + planId: string; + revision: number; + closed: boolean; + startedAt: string | null; + finishedAt?: string | null; + closedAt: string | null; + document: { + title: string; + description?: string; + presentation?: { label: string; detail?: string }; + items: Array< + | (AgentPlanItemDto & { kind: 'task'; fallbackFor?: string }) + | (AgentPlanItemDto & { + kind: 'group'; + tasks: Array; + }) + >; + }; +} + +export interface AgentTaskStopFailure { + jobId: string; + title: string; +} + +export interface AgentTaskCancellationState { + planId: string | null; + status: 'stopping' | 'failed' | 'stopped' | 'resumed'; + reportFailed?: boolean; + requestedAt: string; + failures: AgentTaskStopFailure[]; + plan: AgentPlanSnapshotDto | null; +} diff --git a/packages/@n8n/db/src/migrations/common/1792083059203-AddSourceExecutionToAgentBackgroundJob.ts b/packages/@n8n/db/src/migrations/common/1792083059203-AddSourceExecutionToAgentBackgroundJob.ts new file mode 100644 index 00000000000..6bb610b7140 --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1792083059203-AddSourceExecutionToAgentBackgroundJob.ts @@ -0,0 +1,28 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +export class AddSourceExecutionToAgentBackgroundJob1792083059203 implements ReversibleMigration { + async up({ + schemaBuilder: { addColumns, addForeignKey, column, createIndex }, + }: MigrationContext) { + await addColumns( + 'agent_background_job', + [column('sourceExecutionId').varchar(36).comment('Execution that dispatched this job')], + { recreatesOnSqlite: true }, + ); + await addForeignKey( + 'agent_background_job', + 'sourceExecutionId', + ['agent_execution', 'id'], + undefined, + 'SET NULL', + ); + await createIndex('agent_background_job', ['sourceExecutionId']); + } + + async down({ schemaBuilder: { dropColumns, dropIndex } }: MigrationContext) { + await dropIndex('agent_background_job', ['sourceExecutionId']); + await dropColumns('agent_background_job', ['sourceExecutionId'], { + recreatesOnSqlite: true, + }); + } +} diff --git a/packages/@n8n/db/src/migrations/common/1792083059437-AddTaskStopToAgentExecutionThreads.ts b/packages/@n8n/db/src/migrations/common/1792083059437-AddTaskStopToAgentExecutionThreads.ts new file mode 100644 index 00000000000..f26e8a282d0 --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1792083059437-AddTaskStopToAgentExecutionThreads.ts @@ -0,0 +1,28 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +export class AddTaskStopToAgentExecutionThreads1792083059437 implements ReversibleMigration { + async up({ + schemaBuilder: { addColumns, column }, + isSqlite, + escape, + runQuery, + }: MigrationContext) { + if (isSqlite) { + await runQuery( + `ALTER TABLE ${escape.tableName('agent_execution_threads')} ADD COLUMN ${escape.columnName('taskStop')} TEXT`, + ); + return; + } + await addColumns( + 'agent_execution_threads', + [column('taskStop').json.comment('Latest task stop boundary and unresolved stop errors')], + { recreatesOnSqlite: true }, + ); + } + + async down({ escape, runQuery }: MigrationContext) { + await runQuery( + `ALTER TABLE ${escape.tableName('agent_execution_threads')} DROP COLUMN ${escape.columnName('taskStop')}`, + ); + } +} diff --git a/packages/@n8n/db/src/migrations/common/1792083059654-AddPlanTaskToAgentBackgroundJob.ts b/packages/@n8n/db/src/migrations/common/1792083059654-AddPlanTaskToAgentBackgroundJob.ts new file mode 100644 index 00000000000..77e400c5af6 --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1792083059654-AddPlanTaskToAgentBackgroundJob.ts @@ -0,0 +1,31 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +export class AddPlanTaskToAgentBackgroundJob1792083059654 implements ReversibleMigration { + async up({ + schemaBuilder: { addColumns, column, addForeignKey, createIndex }, + }: MigrationContext) { + await addColumns( + 'agent_background_job', + [ + column('planId').uuid.comment('Plan that owns this work'), + column('planTaskId').uuid.comment('Task ID in the saved plan document'), + ], + { recreatesOnSqlite: true }, + ); + await addForeignKey( + 'agent_background_job', + 'planId', + ['agent_plan', 'id'], + undefined, + 'SET NULL', + ); + await createIndex('agent_background_job', ['planId', 'planTaskId']); + } + + async down({ schemaBuilder: { dropIndex, dropColumns } }: MigrationContext) { + await dropIndex('agent_background_job', ['planId', 'planTaskId']); + await dropColumns('agent_background_job', ['planId', 'planTaskId'], { + recreatesOnSqlite: true, + }); + } +} diff --git a/packages/cli/src/modules/agents/entities/agent-background-job.entity.ts b/packages/cli/src/modules/agents/entities/agent-background-job.entity.ts index d32e7f1409d..a0695f581c7 100644 --- a/packages/cli/src/modules/agents/entities/agent-background-job.entity.ts +++ b/packages/cli/src/modules/agents/entities/agent-background-job.entity.ts @@ -1,6 +1,8 @@ +import type { AgentPlan } from './agent-plan.entity'; +import type { AgentExecution } from './agent-execution.entity'; import type { AgentBackgroundJobDto } from '@n8n/api-types'; import { DateTimeColumn, WithTimestampsAndStringId } from '@n8n/db'; -import { Column, Entity, Index } from '@n8n/typeorm'; +import { Column, Entity, Index, ManyToOne, JoinColumn, type Relation } from '@n8n/typeorm'; export type AgentBackgroundJobKind = AgentBackgroundJobDto['kind']; export type AgentBackgroundJobStatus = AgentBackgroundJobDto['status']; @@ -15,11 +17,35 @@ export type AgentBackgroundJobStatus = AgentBackgroundJobDto['status']; @Entity({ name: 'agent_background_job' }) @Index(['parentThreadId', 'status']) @Index(['parentAgentId']) +@Index(['sourceExecutionId']) +@Index(['planId', 'planTaskId']) @Index(['settledAt']) @Index(['childExecutionId'], { unique: true, where: '"childExecutionId" IS NOT NULL' }) @Index(['timeoutAt'], { where: "\"status\" IN ('running', 'suspended')" }) @Index(['parentThreadId'], { where: '"status" <> \'running\' AND "notifiedAt" IS NULL' }) export class AgentBackgroundJob extends WithTimestampsAndStringId { + @Column({ + type: 'varchar', + length: 36, + nullable: true, + comment: 'Execution that dispatched this job', + }) + sourceExecutionId: string | null; + + @ManyToOne('AgentExecution', { nullable: true, onDelete: 'SET NULL' }) + @JoinColumn({ name: 'sourceExecutionId' }) + sourceExecution: Relation | null; + + @Column({ type: 'uuid', nullable: true, comment: 'Plan that owns this work' }) + planId: string | null; + + @Column({ type: 'uuid', nullable: true, comment: 'Task ID in the saved plan document' }) + planTaskId: string | null; + + @ManyToOne('AgentPlan', { nullable: true, onDelete: 'SET NULL' }) + @JoinColumn({ name: 'planId' }) + plan: Relation | null; + @Column({ type: 'varchar', length: 16 }) kind: AgentBackgroundJobKind; diff --git a/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts b/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts index 0619a4c1f7d..b4d83c877a2 100644 --- a/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts +++ b/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts @@ -1,9 +1,10 @@ import { AGENT_TASK_ID_MAX_LENGTH } from '@n8n/api-types'; -import { Project, User, WithTimestampsAndStringId } from '@n8n/db'; +import { JsonColumn, Project, User, WithTimestampsAndStringId } from '@n8n/db'; import { Column, Entity, Index, JoinColumn, ManyToOne, type Relation } from '@n8n/typeorm'; import { AgentHistory } from './agent-history.entity'; import { Agent } from './agent.entity'; +import type { StoredAgentTaskStop } from '../types/agent-task-stop'; /** * One conversation between a user and an agent. Aggregates per-session @@ -20,6 +21,9 @@ import { Agent } from './agent.entity'; */ @Entity({ name: 'agent_execution_threads' }) export class AgentExecutionThread extends WithTimestampsAndStringId { + @JsonColumn({ nullable: true, comment: 'Latest task stop boundary and unresolved stop errors' }) + taskStop: StoredAgentTaskStop | null; + @ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' }) @JoinColumn({ name: 'ownerId', foreignKeyConstraintName: 'FK_agent_execution_threads_owner' }) owner: Relation | null; diff --git a/packages/cli/src/modules/agents/types/agent-task-stop.ts b/packages/cli/src/modules/agents/types/agent-task-stop.ts new file mode 100644 index 00000000000..db150c17cff --- /dev/null +++ b/packages/cli/src/modules/agents/types/agent-task-stop.ts @@ -0,0 +1,28 @@ +import type { AgentTaskStopFailure } from '@n8n/api-types'; + +/** Keep the stop and its acknowledgement available between jobs and across restarts. */ +export interface AgentTaskStop { + planId: string | null; + requestedAt: string; + /** Unfinished leaf tasks and empty groups captured when Stop was requested. */ + stoppedTaskIds?: string[]; + /** Explicitly selected workflow tasks that still need a replacement job. */ + restartTaskIds?: string[]; + planRevision?: number; + generation: { executionIds: string[]; jobIds: string[]; threadIds: string[] }; + failures: AgentTaskStopFailure[]; + /** Reuse background checkpoints and deliver one report for this stop. */ + pause: { + id: string; + reportExecutionId?: string; + reportedAt?: string; + reportFailed?: boolean; + /** Chat admission only. This does not resume stopped tasks. */ + resumedAt?: string; + }; +} + +/** Earlier stop records have no pause coordination. */ +export type StoredAgentTaskStop = Omit & { + pause?: AgentTaskStop['pause']; +}; diff --git a/packages/cli/test/migration/1792083059203-add-source-execution-to-agent-background-job.test.ts b/packages/cli/test/migration/1792083059203-add-source-execution-to-agent-background-job.test.ts new file mode 100644 index 00000000000..8a1cdd6d39d --- /dev/null +++ b/packages/cli/test/migration/1792083059203-add-source-execution-to-agent-background-job.test.ts @@ -0,0 +1,128 @@ +import { + createTestMigrationContext, + initDbUpToMigration, + runSingleMigration, + undoLastSingleMigration, + type TestMigrationContext, +} from '@n8n/backend-test-utils'; +import { DbConnection } from '@n8n/db'; +import { Container } from '@n8n/di'; +import { DataSource } from '@n8n/typeorm'; +import { randomUUID } from 'node:crypto'; + +const migrationName = 'AddSourceExecutionToAgentBackgroundJob1792083059203'; + +describe('Agent background job source migration', () => { + const threadId = randomUUID(); + const executionId = randomUUID(); + const jobId = randomUUID(); + + async function withContext(fn: (context: TestMigrationContext) => Promise): Promise { + const context = createTestMigrationContext(Container.get(DataSource)); + try { + return await fn(context); + } finally { + await context.queryRunner.release(); + } + } + + async function insert( + context: TestMigrationContext, + table: string, + values: Record, + ) { + const columns = Object.keys(values); + await context.runQuery( + `INSERT INTO ${context.escape.tableName(table)} + (${columns.map(context.escape.columnName).join(', ')}) + VALUES (${columns.map((name) => `:${name}`).join(', ')})`, + values, + ); + } + + beforeAll(async () => { + await Container.get(DbConnection).init(); + await withContext(async (context) => await context.queryRunner.clearDatabase()); + await initDbUpToMigration(migrationName); + await withContext(async (context) => { + const projectId = randomUUID(); + const agentId = randomUUID(); + await insert(context, 'project', { id: projectId, name: 'Stop test project', type: 'team' }); + await insert(context, 'agents', { + id: agentId, + projectId, + name: 'Stop test agent', + integrations: '[]', + tools: '{}', + skills: '{}', + }); + await insert(context, 'agent_execution_threads', { + id: threadId, + projectId, + agentId, + agentName: 'Stop test agent', + }); + await insert(context, 'agent_execution', { id: executionId, threadId, status: 'success' }); + await insert(context, 'agent_background_job', { + id: jobId, + parentAgentId: agentId, + parentThreadId: threadId, + parentResourceId: 'draft-chat:test', + parentPrincipalHash: 'principal', + title: 'Saved result', + kind: 'workflow', + status: 'completed', + result: '{"completed":true}', + }); + }); + }); + + afterAll(async () => await Container.get(DbConnection).close()); + + it('preserves jobs and supports source deletion, rollback, and reapplication', async () => { + await runSingleMigration(migrationName); + await withContext(async ({ escape, runQuery, queryRunner, tablePrefix }) => { + const table = await queryRunner.getTable(`${tablePrefix}agent_background_job`); + expect(table?.findColumnByName('sourceExecutionId')).toMatchObject({ isNullable: true }); + expect( + table?.foreignKeys.filter((key) => key.columnNames.includes('sourceExecutionId')), + ).toHaveLength(1); + expect( + table?.indices.some((index) => index.columnNames.join(',') === 'sourceExecutionId'), + ).toBe(true); + await runQuery( + `UPDATE ${escape.tableName('agent_background_job')} SET ${escape.columnName('sourceExecutionId')} = :executionId WHERE ${escape.columnName('id')} = :jobId`, + { executionId, jobId }, + ); + await runQuery( + `DELETE FROM ${escape.tableName('agent_execution')} WHERE ${escape.columnName('id')} = :executionId`, + { executionId }, + ); + expect( + await runQuery( + `SELECT ${escape.columnName('sourceExecutionId')}, ${escape.columnName('result')} FROM ${escape.tableName('agent_background_job')}`, + ), + ).toEqual([{ sourceExecutionId: null, result: '{"completed":true}' }]); + }); + await undoLastSingleMigration(); + await withContext(async ({ escape, runQuery, queryRunner, tablePrefix }) => { + expect( + (await queryRunner.getTable(`${tablePrefix}agent_background_job`))?.findColumnByName( + 'sourceExecutionId', + ), + ).toBeUndefined(); + expect( + await runQuery( + `SELECT ${escape.columnName('result')} FROM ${escape.tableName('agent_background_job')}`, + ), + ).toEqual([{ result: '{"completed":true}' }]); + }); + await runSingleMigration(migrationName); + await withContext(async ({ queryRunner, tablePrefix }) => { + const table = await queryRunner.getTable(`${tablePrefix}agent_background_job`); + expect( + table?.foreignKeys.filter((key) => key.columnNames.includes('sourceExecutionId')), + ).toHaveLength(1); + }); + }); +}); diff --git a/packages/cli/test/migration/1792083059437-add-task-stop-to-agent-execution-threads.test.ts b/packages/cli/test/migration/1792083059437-add-task-stop-to-agent-execution-threads.test.ts new file mode 100644 index 00000000000..5583c8f0db8 --- /dev/null +++ b/packages/cli/test/migration/1792083059437-add-task-stop-to-agent-execution-threads.test.ts @@ -0,0 +1,122 @@ +import { + createTestMigrationContext, + initDbUpToMigration, + runSingleMigration, + undoLastSingleMigration, + type TestMigrationContext, +} from '@n8n/backend-test-utils'; +import { DbConnection } from '@n8n/db'; +import { Container } from '@n8n/di'; +import { DataSource } from '@n8n/typeorm'; +import { randomUUID } from 'node:crypto'; + +const migrationName = 'AddTaskStopToAgentExecutionThreads1792083059437'; + +describe('AddTaskStopToAgentExecutionThreads migration', () => { + const threadId = randomUUID(); + const executionId = randomUUID(); + + async function withContext(fn: (context: TestMigrationContext) => Promise): Promise { + const context = createTestMigrationContext(Container.get(DataSource)); + try { + return await fn(context); + } finally { + await context.queryRunner.release(); + } + } + + async function insert( + context: TestMigrationContext, + table: string, + values: Record, + ) { + const columns = Object.keys(values); + await context.runQuery( + `INSERT INTO ${context.escape.tableName(table)} + (${columns.map(context.escape.columnName).join(', ')}) + VALUES (${columns.map((name) => `:${name}`).join(', ')})`, + values, + ); + } + + async function readThreads() { + return await withContext( + async ({ escape, runQuery }) => + await runQuery(`SELECT * FROM ${escape.tableName('agent_execution_threads')}`), + ); + } + + beforeAll(async () => { + await Container.get(DbConnection).init(); + await withContext(async (context) => await context.queryRunner.clearDatabase()); + await initDbUpToMigration(migrationName); + await withContext(async (context) => { + const projectId = randomUUID(); + const agentId = randomUUID(); + await insert(context, 'project', { id: projectId, name: 'Stop test project', type: 'team' }); + await insert(context, 'agents', { + id: agentId, + projectId, + name: 'Stop test agent', + integrations: '[]', + tools: '{}', + skills: '{}', + }); + await insert(context, 'agent_execution_threads', { + id: threadId, + projectId, + agentId, + agentName: 'Stop test agent', + }); + await insert(context, 'agent_execution', { id: executionId, threadId, status: 'success' }); + }); + }); + + afterAll(async () => await Container.get(DbConnection).close()); + + it('preserves chats and their execution records through upgrade, rollback, and reapplication', async () => { + const original = await readThreads(); + await runSingleMigration(migrationName); + await withContext(async ({ escape, runQuery, queryRunner, tablePrefix, isSqlite }) => { + expect( + (await queryRunner.getTable(`${tablePrefix}agent_execution_threads`))?.findColumnByName( + 'taskStop', + ), + ).toMatchObject({ + isNullable: true, + type: isSqlite ? 'text' : 'json', + }); + expect( + await runQuery( + `SELECT ${escape.columnName('taskStop')} FROM ${escape.tableName('agent_execution_threads')}`, + ), + ).toEqual([{ taskStop: null }]); + await runQuery( + `UPDATE ${escape.tableName('agent_execution_threads')} SET ${escape.columnName('taskStop')} = :stop WHERE ${escape.columnName('id')} = :threadId`, + { threadId, stop: JSON.stringify({ requestedAt: '2026-10-08T10:00:00.000Z' }) }, + ); + }); + await undoLastSingleMigration(); + expect(await readThreads()).toEqual(original); + await withContext(async ({ queryRunner, tablePrefix }) => { + expect( + (await queryRunner.getTable(`${tablePrefix}agent_execution_threads`))?.findColumnByName( + 'taskStop', + ), + ).toBeUndefined(); + }); + await runSingleMigration(migrationName); + await withContext(async ({ escape, runQuery, queryRunner, tablePrefix }) => { + expect( + await runQuery( + `SELECT ${escape.columnName('id')} FROM ${escape.tableName('agent_execution')}`, + ), + ).toEqual([{ id: executionId }]); + expect( + (await queryRunner.getTable(`${tablePrefix}agent_execution_threads`))?.findColumnByName( + 'taskStop', + ), + ).toMatchObject({ isNullable: true }); + }); + }); +}); diff --git a/packages/cli/test/migration/1792083059654-add-plan-task-to-agent-background-job.test.ts b/packages/cli/test/migration/1792083059654-add-plan-task-to-agent-background-job.test.ts new file mode 100644 index 00000000000..70b4bbadfaa --- /dev/null +++ b/packages/cli/test/migration/1792083059654-add-plan-task-to-agent-background-job.test.ts @@ -0,0 +1,134 @@ +import { + createTestMigrationContext, + initDbUpToMigration, + runSingleMigration, + undoLastSingleMigration, + type TestMigrationContext, +} from '@n8n/backend-test-utils'; +import { DbConnection } from '@n8n/db'; +import { Container } from '@n8n/di'; +import { DataSource } from '@n8n/typeorm'; +import { randomUUID } from 'node:crypto'; + +const migrationName = 'AddPlanTaskToAgentBackgroundJob1792083059654'; + +describe('Agent background job plan task migration', () => { + const threadId = randomUUID(); + const planId = randomUUID(); + const planTaskId = randomUUID(); + const jobId = randomUUID(); + + async function withContext(fn: (context: TestMigrationContext) => Promise): Promise { + const context = createTestMigrationContext(Container.get(DataSource)); + try { + return await fn(context); + } finally { + await context.queryRunner.release(); + } + } + + async function insert( + context: TestMigrationContext, + table: string, + values: Record, + ) { + const columns = Object.keys(values); + await context.runQuery( + `INSERT INTO ${context.escape.tableName(table)} + (${columns.map(context.escape.columnName).join(', ')}) + VALUES (${columns.map((name) => `:${name}`).join(', ')})`, + values, + ); + } + + beforeAll(async () => { + await Container.get(DbConnection).init(); + await withContext(async (context) => await context.queryRunner.clearDatabase()); + await initDbUpToMigration(migrationName); + await withContext(async (context) => { + const projectId = randomUUID(); + const agentId = randomUUID(); + await insert(context, 'project', { id: projectId, name: 'Stop test project', type: 'team' }); + await insert(context, 'agents', { + id: agentId, + projectId, + name: 'Stop test agent', + integrations: '[]', + tools: '{}', + skills: '{}', + }); + await insert(context, 'agent_execution_threads', { + id: threadId, + projectId, + agentId, + agentName: 'Stop test agent', + }); + await insert(context, 'agent_plan', { id: planId, threadId, formatVersion: '1', data: '{}' }); + await insert(context, 'agent_background_job', { + id: jobId, + parentAgentId: agentId, + parentThreadId: threadId, + parentResourceId: 'draft-chat:test', + parentPrincipalHash: 'principal', + title: 'Saved result', + kind: 'workflow', + status: 'completed', + result: '{"completed":true}', + }); + }); + }); + + afterAll(async () => await Container.get(DbConnection).close()); + + it('preserves jobs and supports plan deletion, rollback, and reapplication', async () => { + await runSingleMigration(migrationName); + await withContext(async ({ escape, runQuery, queryRunner, tablePrefix }) => { + const table = await queryRunner.getTable(`${tablePrefix}agent_background_job`); + expect(table?.findColumnByName('planId')).toMatchObject({ isNullable: true }); + expect(table?.findColumnByName('planTaskId')).toMatchObject({ isNullable: true }); + expect( + table?.indices.some((index) => index.columnNames.join(',') === 'planId,planTaskId'), + ).toBe(true); + expect(table?.foreignKeys.filter((key) => key.columnNames.includes('planId'))).toHaveLength( + 1, + ); + expect(table?.findColumnByName('detached')).toBeUndefined(); + expect( + (await queryRunner.getTable(`${tablePrefix}agent_message_queue`))?.findColumnByName('held'), + ).toBeUndefined(); + await runQuery( + `UPDATE ${escape.tableName('agent_background_job')} SET ${escape.columnName('planId')} = :planId, ${escape.columnName('planTaskId')} = :planTaskId WHERE ${escape.columnName('id')} = :jobId`, + { planId, planTaskId, jobId }, + ); + await runQuery( + `DELETE FROM ${escape.tableName('agent_plan')} WHERE ${escape.columnName('id')} = :planId`, + { planId }, + ); + expect( + await runQuery( + `SELECT ${escape.columnName('planId')}, ${escape.columnName('result')} FROM ${escape.tableName('agent_background_job')}`, + ), + ).toEqual([{ planId: null, result: '{"completed":true}' }]); + }); + await undoLastSingleMigration(); + await withContext(async ({ escape, runQuery, queryRunner, tablePrefix }) => { + expect( + (await queryRunner.getTable(`${tablePrefix}agent_background_job`))?.findColumnByName( + 'planId', + ), + ).toBeUndefined(); + expect( + await runQuery( + `SELECT ${escape.columnName('result')} FROM ${escape.tableName('agent_background_job')}`, + ), + ).toEqual([{ result: '{"completed":true}' }]); + }); + await runSingleMigration(migrationName); + await withContext(async ({ queryRunner, tablePrefix }) => { + const table = await queryRunner.getTable(`${tablePrefix}agent_background_job`); + expect(table?.foreignKeys.filter((key) => key.columnNames.includes('planId'))).toHaveLength( + 1, + ); + }); + }); +});