diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts
index 0192820eb..0c786464e 100644
--- a/apps/cli/src/main.ts
+++ b/apps/cli/src/main.ts
@@ -394,6 +394,7 @@ const executeCode = (input: {
payload: {
code: input.code,
},
+ headers: { "x-executor-trigger": "cli" },
});
if (response.status === "paused") {
diff --git a/apps/cloud/drizzle/0006_panoramic_mother_askani.sql b/apps/cloud/drizzle/0006_panoramic_mother_askani.sql
new file mode 100644
index 000000000..0c708a690
--- /dev/null
+++ b/apps/cloud/drizzle/0006_panoramic_mother_askani.sql
@@ -0,0 +1,54 @@
+CREATE TABLE "execution" (
+ "id" text NOT NULL,
+ "scope_id" text NOT NULL,
+ "status" text NOT NULL,
+ "code" text NOT NULL,
+ "result_json" text,
+ "error_text" text,
+ "logs_json" text,
+ "started_at" bigint,
+ "completed_at" bigint,
+ "trigger_kind" text,
+ "trigger_meta_json" text,
+ "tool_call_count" bigint DEFAULT 0 NOT NULL,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL,
+ CONSTRAINT "execution_scope_id_id_pk" PRIMARY KEY("scope_id","id")
+);
+--> statement-breakpoint
+CREATE TABLE "execution_interaction" (
+ "id" text PRIMARY KEY NOT NULL,
+ "execution_id" text NOT NULL,
+ "status" text NOT NULL,
+ "kind" text NOT NULL,
+ "purpose" text,
+ "payload_json" text,
+ "response_json" text,
+ "response_private_json" text,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "execution_tool_call" (
+ "id" text PRIMARY KEY NOT NULL,
+ "execution_id" text NOT NULL,
+ "status" text NOT NULL,
+ "tool_path" text NOT NULL,
+ "namespace" text,
+ "args_json" text,
+ "result_json" text,
+ "error_text" text,
+ "started_at" bigint NOT NULL,
+ "completed_at" bigint,
+ "duration_ms" bigint
+);
+--> statement-breakpoint
+CREATE INDEX "execution_scope_id_idx" ON "execution" USING btree ("scope_id");--> statement-breakpoint
+CREATE INDEX "execution_status_idx" ON "execution" USING btree ("status");--> statement-breakpoint
+CREATE INDEX "execution_trigger_kind_idx" ON "execution" USING btree ("trigger_kind");--> statement-breakpoint
+CREATE INDEX "execution_created_at_idx" ON "execution" USING btree ("created_at");--> statement-breakpoint
+CREATE INDEX "execution_interaction_execution_id_idx" ON "execution_interaction" USING btree ("execution_id");--> statement-breakpoint
+CREATE INDEX "execution_interaction_status_idx" ON "execution_interaction" USING btree ("status");--> statement-breakpoint
+CREATE INDEX "execution_tool_call_execution_id_idx" ON "execution_tool_call" USING btree ("execution_id");--> statement-breakpoint
+CREATE INDEX "execution_tool_call_tool_path_idx" ON "execution_tool_call" USING btree ("tool_path");--> statement-breakpoint
+CREATE INDEX "execution_tool_call_namespace_idx" ON "execution_tool_call" USING btree ("namespace");
diff --git a/apps/cloud/drizzle/meta/0006_snapshot.json b/apps/cloud/drizzle/meta/0006_snapshot.json
new file mode 100644
index 000000000..a591550d1
--- /dev/null
+++ b/apps/cloud/drizzle/meta/0006_snapshot.json
@@ -0,0 +1,1940 @@
+{
+ "id": "dbf9e784-c297-426c-b602-14a30cf54c64",
+ "prevId": "09d08343-8162-4e6b-91ab-ce0a9d6bad10",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounts": {
+ "name": "accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memberships": {
+ "name": "memberships",
+ "schema": "",
+ "columns": {
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "memberships_account_id_accounts_id_fk": {
+ "name": "memberships_account_id_accounts_id_fk",
+ "tableFrom": "memberships",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "memberships_organization_id_organizations_id_fk": {
+ "name": "memberships_organization_id_organizations_id_fk",
+ "tableFrom": "memberships",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "memberships_account_id_organization_id_pk": {
+ "name": "memberships_account_id_organization_id_pk",
+ "columns": [
+ "account_id",
+ "organization_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organizations": {
+ "name": "organizations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.blob": {
+ "name": "blob",
+ "schema": "",
+ "columns": {
+ "namespace": {
+ "name": "namespace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "blob_namespace_key_pk": {
+ "name": "blob_namespace_key_pk",
+ "columns": [
+ "namespace",
+ "key"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.connection": {
+ "name": "connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identity_label": {
+ "name": "identity_label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_secret_id": {
+ "name": "access_token_secret_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token_secret_id": {
+ "name": "refresh_token_secret_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_state": {
+ "name": "provider_state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "connection_scope_id_idx": {
+ "name": "connection_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "connection_provider_idx": {
+ "name": "connection_provider_idx",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "connection_scope_id_id_pk": {
+ "name": "connection_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.definition": {
+ "name": "definition",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "definition_scope_id_idx": {
+ "name": "definition_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "definition_source_id_idx": {
+ "name": "definition_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "definition_plugin_id_idx": {
+ "name": "definition_plugin_id_idx",
+ "columns": [
+ {
+ "expression": "plugin_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "definition_scope_id_id_pk": {
+ "name": "definition_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution": {
+ "name": "execution",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "result_json": {
+ "name": "result_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_text": {
+ "name": "error_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logs_json": {
+ "name": "logs_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_kind": {
+ "name": "trigger_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_meta_json": {
+ "name": "trigger_meta_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tool_call_count": {
+ "name": "tool_call_count",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "execution_scope_id_idx": {
+ "name": "execution_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_status_idx": {
+ "name": "execution_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_trigger_kind_idx": {
+ "name": "execution_trigger_kind_idx",
+ "columns": [
+ {
+ "expression": "trigger_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_created_at_idx": {
+ "name": "execution_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "execution_scope_id_id_pk": {
+ "name": "execution_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution_interaction": {
+ "name": "execution_interaction",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload_json": {
+ "name": "payload_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_json": {
+ "name": "response_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_private_json": {
+ "name": "response_private_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "execution_interaction_execution_id_idx": {
+ "name": "execution_interaction_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_interaction_status_idx": {
+ "name": "execution_interaction_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution_tool_call": {
+ "name": "execution_tool_call",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_path": {
+ "name": "tool_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "namespace": {
+ "name": "namespace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "args_json": {
+ "name": "args_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result_json": {
+ "name": "result_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_text": {
+ "name": "error_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "execution_tool_call_execution_id_idx": {
+ "name": "execution_tool_call_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_tool_call_tool_path_idx": {
+ "name": "execution_tool_call_tool_path_idx",
+ "columns": [
+ {
+ "expression": "tool_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_tool_call_namespace_idx": {
+ "name": "execution_tool_call_namespace_idx",
+ "columns": [
+ {
+ "expression": "namespace",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.graphql_operation": {
+ "name": "graphql_operation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "binding": {
+ "name": "binding",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "graphql_operation_scope_id_idx": {
+ "name": "graphql_operation_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "graphql_operation_source_id_idx": {
+ "name": "graphql_operation_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "graphql_operation_scope_id_id_pk": {
+ "name": "graphql_operation_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.graphql_source": {
+ "name": "graphql_source",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "headers": {
+ "name": "headers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "graphql_source_scope_id_idx": {
+ "name": "graphql_source_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "graphql_source_scope_id_id_pk": {
+ "name": "graphql_source_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_binding": {
+ "name": "mcp_binding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "binding": {
+ "name": "binding",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "mcp_binding_scope_id_idx": {
+ "name": "mcp_binding_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_binding_source_id_idx": {
+ "name": "mcp_binding_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_binding_scope_id_id_pk": {
+ "name": "mcp_binding_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_oauth_session": {
+ "name": "mcp_oauth_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session": {
+ "name": "session",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "mcp_oauth_session_scope_id_idx": {
+ "name": "mcp_oauth_session_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_oauth_session_scope_id_id_pk": {
+ "name": "mcp_oauth_session_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_source": {
+ "name": "mcp_source",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "mcp_source_scope_id_idx": {
+ "name": "mcp_source_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_source_scope_id_id_pk": {
+ "name": "mcp_source_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.openapi_oauth_session": {
+ "name": "openapi_oauth_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session": {
+ "name": "session",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "openapi_oauth_session_scope_id_idx": {
+ "name": "openapi_oauth_session_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "openapi_oauth_session_scope_id_id_pk": {
+ "name": "openapi_oauth_session_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.openapi_operation": {
+ "name": "openapi_operation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "binding": {
+ "name": "binding",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "openapi_operation_scope_id_idx": {
+ "name": "openapi_operation_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "openapi_operation_source_id_idx": {
+ "name": "openapi_operation_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "openapi_operation_scope_id_id_pk": {
+ "name": "openapi_operation_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.openapi_source": {
+ "name": "openapi_source",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "spec": {
+ "name": "spec",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "base_url": {
+ "name": "base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth2": {
+ "name": "oauth2",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "invocation_config": {
+ "name": "invocation_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "openapi_source_scope_id_idx": {
+ "name": "openapi_source_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "openapi_source_scope_id_id_pk": {
+ "name": "openapi_source_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.openapi_source_binding": {
+ "name": "openapi_source_binding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_scope_id": {
+ "name": "source_scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_scope_id": {
+ "name": "target_scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slot": {
+ "name": "slot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "openapi_source_binding_source_id_idx": {
+ "name": "openapi_source_binding_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "openapi_source_binding_source_scope_id_idx": {
+ "name": "openapi_source_binding_source_scope_id_idx",
+ "columns": [
+ {
+ "expression": "source_scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "openapi_source_binding_target_scope_id_idx": {
+ "name": "openapi_source_binding_target_scope_id_idx",
+ "columns": [
+ {
+ "expression": "target_scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "openapi_source_binding_slot_idx": {
+ "name": "openapi_source_binding_slot_idx",
+ "columns": [
+ {
+ "expression": "slot",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "openapi_source_binding_id_pk": {
+ "name": "openapi_source_binding_id_pk",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.secret": {
+ "name": "secret",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owned_by_connection_id": {
+ "name": "owned_by_connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "secret_scope_id_idx": {
+ "name": "secret_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "secret_provider_idx": {
+ "name": "secret_provider_idx",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "secret_owned_by_connection_id_idx": {
+ "name": "secret_owned_by_connection_id_idx",
+ "columns": [
+ {
+ "expression": "owned_by_connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "secret_scope_id_id_pk": {
+ "name": "secret_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.source": {
+ "name": "source",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "can_remove": {
+ "name": "can_remove",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "can_refresh": {
+ "name": "can_refresh",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "can_edit": {
+ "name": "can_edit",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "source_scope_id_idx": {
+ "name": "source_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "source_plugin_id_idx": {
+ "name": "source_plugin_id_idx",
+ "columns": [
+ {
+ "expression": "plugin_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "source_scope_id_id_pk": {
+ "name": "source_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tool": {
+ "name": "tool",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "input_schema": {
+ "name": "input_schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "output_schema": {
+ "name": "output_schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "tool_scope_id_idx": {
+ "name": "tool_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tool_source_id_idx": {
+ "name": "tool_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tool_plugin_id_idx": {
+ "name": "tool_plugin_id_idx",
+ "columns": [
+ {
+ "expression": "plugin_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "tool_scope_id_id_pk": {
+ "name": "tool_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workos_vault_metadata": {
+ "name": "workos_vault_metadata",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "workos_vault_metadata_scope_id_idx": {
+ "name": "workos_vault_metadata_scope_id_idx",
+ "columns": [
+ {
+ "expression": "scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "workos_vault_metadata_scope_id_id_pk": {
+ "name": "workos_vault_metadata_scope_id_id_pk",
+ "columns": [
+ "scope_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json
index 96a200266..c17835d8a 100644
--- a/apps/cloud/drizzle/meta/_journal.json
+++ b/apps/cloud/drizzle/meta/_journal.json
@@ -43,6 +43,13 @@
"when": 1777000000000,
"tag": "0005_drop_connection_kind",
"breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "7",
+ "when": 1777044250244,
+ "tag": "0006_panoramic_mother_askani",
+ "breakpoints": true
}
]
-}
+}
\ No newline at end of file
diff --git a/apps/cloud/src/routeTree.gen.ts b/apps/cloud/src/routeTree.gen.ts
index 46104e23b..60dd41900 100644
--- a/apps/cloud/src/routeTree.gen.ts
+++ b/apps/cloud/src/routeTree.gen.ts
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as ToolsRouteImport } from './routes/tools'
import { Route as SecretsRouteImport } from './routes/secrets'
+import { Route as RunsRouteImport } from './routes/runs'
import { Route as OrgRouteImport } from './routes/org'
import { Route as ConnectionsRouteImport } from './routes/connections'
import { Route as BillingRouteImport } from './routes/billing'
@@ -29,6 +30,11 @@ const SecretsRoute = SecretsRouteImport.update({
path: '/secrets',
getParentRoute: () => rootRouteImport,
} as any)
+const RunsRoute = RunsRouteImport.update({
+ id: '/runs',
+ path: '/runs',
+ getParentRoute: () => rootRouteImport,
+} as any)
const OrgRoute = OrgRouteImport.update({
id: '/org',
path: '/org',
@@ -70,6 +76,7 @@ export interface FileRoutesByFullPath {
'/billing': typeof BillingRoute
'/connections': typeof ConnectionsRoute
'/org': typeof OrgRoute
+ '/runs': typeof RunsRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
'/billing/plans': typeof BillingPlansRoute
@@ -81,6 +88,7 @@ export interface FileRoutesByTo {
'/billing': typeof BillingRoute
'/connections': typeof ConnectionsRoute
'/org': typeof OrgRoute
+ '/runs': typeof RunsRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
'/billing/plans': typeof BillingPlansRoute
@@ -93,6 +101,7 @@ export interface FileRoutesById {
'/billing': typeof BillingRoute
'/connections': typeof ConnectionsRoute
'/org': typeof OrgRoute
+ '/runs': typeof RunsRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
'/billing_/plans': typeof BillingPlansRoute
@@ -106,6 +115,7 @@ export interface FileRouteTypes {
| '/billing'
| '/connections'
| '/org'
+ | '/runs'
| '/secrets'
| '/tools'
| '/billing/plans'
@@ -117,6 +127,7 @@ export interface FileRouteTypes {
| '/billing'
| '/connections'
| '/org'
+ | '/runs'
| '/secrets'
| '/tools'
| '/billing/plans'
@@ -128,6 +139,7 @@ export interface FileRouteTypes {
| '/billing'
| '/connections'
| '/org'
+ | '/runs'
| '/secrets'
| '/tools'
| '/billing_/plans'
@@ -140,6 +152,7 @@ export interface RootRouteChildren {
BillingRoute: typeof BillingRoute
ConnectionsRoute: typeof ConnectionsRoute
OrgRoute: typeof OrgRoute
+ RunsRoute: typeof RunsRoute
SecretsRoute: typeof SecretsRoute
ToolsRoute: typeof ToolsRoute
BillingPlansRoute: typeof BillingPlansRoute
@@ -163,6 +176,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SecretsRouteImport
parentRoute: typeof rootRouteImport
}
+ '/runs': {
+ id: '/runs'
+ path: '/runs'
+ fullPath: '/runs'
+ preLoaderRoute: typeof RunsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/org': {
id: '/org'
path: '/org'
@@ -220,6 +240,7 @@ const rootRouteChildren: RootRouteChildren = {
BillingRoute: BillingRoute,
ConnectionsRoute: ConnectionsRoute,
OrgRoute: OrgRoute,
+ RunsRoute: RunsRoute,
SecretsRoute: SecretsRoute,
ToolsRoute: ToolsRoute,
BillingPlansRoute: BillingPlansRoute,
diff --git a/apps/cloud/src/routes/runs.tsx b/apps/cloud/src/routes/runs.tsx
new file mode 100644
index 000000000..175234c0e
--- /dev/null
+++ b/apps/cloud/src/routes/runs.tsx
@@ -0,0 +1,24 @@
+import { Schema } from "effect";
+import { createFileRoute } from "@tanstack/react-router";
+import { RunsPage, type RunsSearch } from "@executor/react/pages/runs";
+
+const RunsSearchSchema = Schema.standardSchemaV1(
+ Schema.Struct({
+ executionId: Schema.optional(Schema.String),
+ status: Schema.optional(Schema.String),
+ trigger: Schema.optional(Schema.String),
+ tool: Schema.optional(Schema.String),
+ range: Schema.optional(Schema.String),
+ from: Schema.optional(Schema.String),
+ to: Schema.optional(Schema.String),
+ code: Schema.optional(Schema.String),
+ live: Schema.optional(Schema.String),
+ sort: Schema.optional(Schema.String),
+ elicitation: Schema.optional(Schema.String),
+ }),
+);
+
+export const Route = createFileRoute("/runs")({
+ validateSearch: RunsSearchSchema,
+ component: () => ,
+});
diff --git a/apps/cloud/src/services/executor-schema.ts b/apps/cloud/src/services/executor-schema.ts
index d0624a510..84fe9afe7 100644
--- a/apps/cloud/src/services/executor-schema.ts
+++ b/apps/cloud/src/services/executor-schema.ts
@@ -203,6 +203,74 @@ export const workos_vault_metadata = pgTable("workos_vault_metadata", {
index("workos_vault_metadata_scope_id_idx").on(table.scope_id),
]);
+// Execution history — one row per engine.execute() / executeWithPause()
+// call. `scope_id` is the innermost executor scope that owned the run;
+// the scoped adapter filters these on every list query. JSON-bearing
+// columns (result/error/logs/trigger-meta) are text blobs; the SDK
+// never parses them server-side.
+export const execution = pgTable("execution", {
+ id: text('id').notNull(),
+ scope_id: text('scope_id').notNull(),
+ status: text('status').notNull(),
+ code: text('code').notNull(),
+ result_json: text('result_json'),
+ error_text: text('error_text'),
+ logs_json: text('logs_json'),
+ started_at: bigint('started_at', { mode: 'number' }),
+ completed_at: bigint('completed_at', { mode: 'number' }),
+ trigger_kind: text('trigger_kind'),
+ trigger_meta_json: text('trigger_meta_json'),
+ tool_call_count: bigint('tool_call_count', { mode: 'number' }).default(0).notNull(),
+ created_at: timestamp('created_at').notNull(),
+ updated_at: timestamp('updated_at').notNull()
+}, (table) => [
+ primaryKey({ columns: [table.scope_id, table.id] }),
+ index("execution_scope_id_idx").on(table.scope_id),
+ index("execution_status_idx").on(table.status),
+ index("execution_trigger_kind_idx").on(table.trigger_kind),
+ index("execution_created_at_idx").on(table.created_at),
+]);
+
+// Per-execution interaction rows — elicitation requests + their
+// resolutions. Not scope-owned; tenant isolation flows through the
+// parent execution.
+export const execution_interaction = pgTable("execution_interaction", {
+ id: text('id').primaryKey(),
+ execution_id: text('execution_id').notNull(),
+ status: text('status').notNull(),
+ kind: text('kind').notNull(),
+ purpose: text('purpose'),
+ payload_json: text('payload_json'),
+ response_json: text('response_json'),
+ response_private_json: text('response_private_json'),
+ created_at: timestamp('created_at').notNull(),
+ updated_at: timestamp('updated_at').notNull()
+}, (table) => [
+ index("execution_interaction_execution_id_idx").on(table.execution_id),
+ index("execution_interaction_status_idx").on(table.status),
+]);
+
+// Per-execution tool-call rows — one per executor.tools.invoke call
+// inside the sandboxed execution. Powers the runs UI's tool-call
+// timeline + facet list.
+export const execution_tool_call = pgTable("execution_tool_call", {
+ id: text('id').primaryKey(),
+ execution_id: text('execution_id').notNull(),
+ status: text('status').notNull(),
+ tool_path: text('tool_path').notNull(),
+ namespace: text('namespace'),
+ args_json: text('args_json'),
+ result_json: text('result_json'),
+ error_text: text('error_text'),
+ started_at: bigint('started_at', { mode: 'number' }).notNull(),
+ completed_at: bigint('completed_at', { mode: 'number' }),
+ duration_ms: bigint('duration_ms', { mode: 'number' })
+}, (table) => [
+ index("execution_tool_call_execution_id_idx").on(table.execution_id),
+ index("execution_tool_call_tool_path_idx").on(table.tool_path),
+ index("execution_tool_call_namespace_idx").on(table.namespace),
+]);
+
// Blob store table — hand-appended. BlobStore is a separate storage
// abstraction from DBSchema, so the CLI doesn't generate it. Keep in
// sync with @executor/storage-postgres's BlobStore implementation.
diff --git a/apps/cloud/src/web/shell.tsx b/apps/cloud/src/web/shell.tsx
index 4aa725027..9d685ede3 100644
--- a/apps/cloud/src/web/shell.tsx
+++ b/apps/cloud/src/web/shell.tsx
@@ -360,6 +360,7 @@ function UserFooter() {
function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean }) {
const isHome = props.pathname === "/";
const isSecrets = props.pathname === "/secrets";
+ const isRuns = props.pathname === "/runs";
const isConnections = props.pathname === "/connections";
const isBilling = props.pathname === "/billing" || props.pathname.startsWith("/billing/");
const isOrg = props.pathname === "/org";
@@ -378,6 +379,7 @@ function SidebarContent(props: { pathname: string; onNavigate?: () => void; show
+
diff --git a/apps/local/drizzle/0004_fancy_red_wolf.sql b/apps/local/drizzle/0004_fancy_red_wolf.sql
new file mode 100644
index 000000000..0c7b03703
--- /dev/null
+++ b/apps/local/drizzle/0004_fancy_red_wolf.sql
@@ -0,0 +1,54 @@
+CREATE TABLE `execution` (
+ `id` text NOT NULL,
+ `scope_id` text NOT NULL,
+ `status` text NOT NULL,
+ `code` text NOT NULL,
+ `result_json` text,
+ `error_text` text,
+ `logs_json` text,
+ `started_at` integer,
+ `completed_at` integer,
+ `trigger_kind` text,
+ `trigger_meta_json` text,
+ `tool_call_count` integer DEFAULT 0 NOT NULL,
+ `created_at` integer NOT NULL,
+ `updated_at` integer NOT NULL,
+ PRIMARY KEY(`scope_id`, `id`)
+);
+--> statement-breakpoint
+CREATE INDEX `execution_scope_id_idx` ON `execution` (`scope_id`);--> statement-breakpoint
+CREATE INDEX `execution_status_idx` ON `execution` (`status`);--> statement-breakpoint
+CREATE INDEX `execution_trigger_kind_idx` ON `execution` (`trigger_kind`);--> statement-breakpoint
+CREATE INDEX `execution_created_at_idx` ON `execution` (`created_at`);--> statement-breakpoint
+CREATE TABLE `execution_interaction` (
+ `id` text PRIMARY KEY NOT NULL,
+ `execution_id` text NOT NULL,
+ `status` text NOT NULL,
+ `kind` text NOT NULL,
+ `purpose` text,
+ `payload_json` text,
+ `response_json` text,
+ `response_private_json` text,
+ `created_at` integer NOT NULL,
+ `updated_at` integer NOT NULL
+);
+--> statement-breakpoint
+CREATE INDEX `execution_interaction_execution_id_idx` ON `execution_interaction` (`execution_id`);--> statement-breakpoint
+CREATE INDEX `execution_interaction_status_idx` ON `execution_interaction` (`status`);--> statement-breakpoint
+CREATE TABLE `execution_tool_call` (
+ `id` text PRIMARY KEY NOT NULL,
+ `execution_id` text NOT NULL,
+ `status` text NOT NULL,
+ `tool_path` text NOT NULL,
+ `namespace` text,
+ `args_json` text,
+ `result_json` text,
+ `error_text` text,
+ `started_at` integer NOT NULL,
+ `completed_at` integer,
+ `duration_ms` integer
+);
+--> statement-breakpoint
+CREATE INDEX `execution_tool_call_execution_id_idx` ON `execution_tool_call` (`execution_id`);--> statement-breakpoint
+CREATE INDEX `execution_tool_call_tool_path_idx` ON `execution_tool_call` (`tool_path`);--> statement-breakpoint
+CREATE INDEX `execution_tool_call_namespace_idx` ON `execution_tool_call` (`namespace`);
\ No newline at end of file
diff --git a/apps/local/drizzle/meta/0004_snapshot.json b/apps/local/drizzle/meta/0004_snapshot.json
new file mode 100644
index 000000000..93d79f524
--- /dev/null
+++ b/apps/local/drizzle/meta/0004_snapshot.json
@@ -0,0 +1,1673 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "808d695c-f4c8-43a1-a75e-1c87009edff6",
+ "prevId": "b20a0eff-12a3-4709-9389-4353e5191535",
+ "tables": {
+ "connection": {
+ "name": "connection",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "identity_label": {
+ "name": "identity_label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token_secret_id": {
+ "name": "access_token_secret_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token_secret_id": {
+ "name": "refresh_token_secret_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider_state": {
+ "name": "provider_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "connection_scope_id_idx": {
+ "name": "connection_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "connection_provider_idx": {
+ "name": "connection_provider_idx",
+ "columns": [
+ "provider"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "connection_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "connection_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "definition": {
+ "name": "definition",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "schema": {
+ "name": "schema",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "definition_scope_id_idx": {
+ "name": "definition_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "definition_source_id_idx": {
+ "name": "definition_source_id_idx",
+ "columns": [
+ "source_id"
+ ],
+ "isUnique": false
+ },
+ "definition_plugin_id_idx": {
+ "name": "definition_plugin_id_idx",
+ "columns": [
+ "plugin_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "definition_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "definition_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "execution": {
+ "name": "execution",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "result_json": {
+ "name": "result_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_text": {
+ "name": "error_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "logs_json": {
+ "name": "logs_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "trigger_kind": {
+ "name": "trigger_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "trigger_meta_json": {
+ "name": "trigger_meta_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tool_call_count": {
+ "name": "tool_call_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "execution_scope_id_idx": {
+ "name": "execution_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "execution_status_idx": {
+ "name": "execution_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ },
+ "execution_trigger_kind_idx": {
+ "name": "execution_trigger_kind_idx",
+ "columns": [
+ "trigger_kind"
+ ],
+ "isUnique": false
+ },
+ "execution_created_at_idx": {
+ "name": "execution_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "execution_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "execution_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "execution_interaction": {
+ "name": "execution_interaction",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "payload_json": {
+ "name": "payload_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "response_json": {
+ "name": "response_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "response_private_json": {
+ "name": "response_private_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "execution_interaction_execution_id_idx": {
+ "name": "execution_interaction_execution_id_idx",
+ "columns": [
+ "execution_id"
+ ],
+ "isUnique": false
+ },
+ "execution_interaction_status_idx": {
+ "name": "execution_interaction_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "execution_tool_call": {
+ "name": "execution_tool_call",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tool_path": {
+ "name": "tool_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "namespace": {
+ "name": "namespace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "args_json": {
+ "name": "args_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "result_json": {
+ "name": "result_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_text": {
+ "name": "error_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "execution_tool_call_execution_id_idx": {
+ "name": "execution_tool_call_execution_id_idx",
+ "columns": [
+ "execution_id"
+ ],
+ "isUnique": false
+ },
+ "execution_tool_call_tool_path_idx": {
+ "name": "execution_tool_call_tool_path_idx",
+ "columns": [
+ "tool_path"
+ ],
+ "isUnique": false
+ },
+ "execution_tool_call_namespace_idx": {
+ "name": "execution_tool_call_namespace_idx",
+ "columns": [
+ "namespace"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "google_discovery_binding": {
+ "name": "google_discovery_binding",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "binding": {
+ "name": "binding",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "google_discovery_binding_scope_id_idx": {
+ "name": "google_discovery_binding_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "google_discovery_binding_source_id_idx": {
+ "name": "google_discovery_binding_source_id_idx",
+ "columns": [
+ "source_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "google_discovery_binding_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "google_discovery_binding_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "google_discovery_oauth_session": {
+ "name": "google_discovery_oauth_session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "session": {
+ "name": "session",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "google_discovery_oauth_session_scope_id_idx": {
+ "name": "google_discovery_oauth_session_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "google_discovery_oauth_session_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "google_discovery_oauth_session_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "google_discovery_source": {
+ "name": "google_discovery_source",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "google_discovery_source_scope_id_idx": {
+ "name": "google_discovery_source_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "google_discovery_source_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "google_discovery_source_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "graphql_operation": {
+ "name": "graphql_operation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "binding": {
+ "name": "binding",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "graphql_operation_scope_id_idx": {
+ "name": "graphql_operation_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "graphql_operation_source_id_idx": {
+ "name": "graphql_operation_source_id_idx",
+ "columns": [
+ "source_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "graphql_operation_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "graphql_operation_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "graphql_source": {
+ "name": "graphql_source",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "graphql_source_scope_id_idx": {
+ "name": "graphql_source_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "graphql_source_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "graphql_source_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "mcp_binding": {
+ "name": "mcp_binding",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "binding": {
+ "name": "binding",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "mcp_binding_scope_id_idx": {
+ "name": "mcp_binding_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "mcp_binding_source_id_idx": {
+ "name": "mcp_binding_source_id_idx",
+ "columns": [
+ "source_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_binding_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "mcp_binding_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "mcp_oauth_session": {
+ "name": "mcp_oauth_session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "session": {
+ "name": "session",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "mcp_oauth_session_scope_id_idx": {
+ "name": "mcp_oauth_session_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_oauth_session_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "mcp_oauth_session_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "mcp_source": {
+ "name": "mcp_source",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "mcp_source_scope_id_idx": {
+ "name": "mcp_source_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_source_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "mcp_source_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "openapi_oauth_session": {
+ "name": "openapi_oauth_session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "session": {
+ "name": "session",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "openapi_oauth_session_scope_id_idx": {
+ "name": "openapi_oauth_session_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "openapi_oauth_session_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "openapi_oauth_session_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "openapi_operation": {
+ "name": "openapi_operation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "binding": {
+ "name": "binding",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "openapi_operation_scope_id_idx": {
+ "name": "openapi_operation_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "openapi_operation_source_id_idx": {
+ "name": "openapi_operation_source_id_idx",
+ "columns": [
+ "source_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "openapi_operation_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "openapi_operation_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "openapi_source": {
+ "name": "openapi_source",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "spec": {
+ "name": "spec",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "base_url": {
+ "name": "base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "oauth2": {
+ "name": "oauth2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "invocation_config": {
+ "name": "invocation_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "openapi_source_scope_id_idx": {
+ "name": "openapi_source_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "openapi_source_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "openapi_source_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "openapi_source_binding": {
+ "name": "openapi_source_binding",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_scope_id": {
+ "name": "source_scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "target_scope_id": {
+ "name": "target_scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slot": {
+ "name": "slot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "openapi_source_binding_source_id_idx": {
+ "name": "openapi_source_binding_source_id_idx",
+ "columns": [
+ "source_id"
+ ],
+ "isUnique": false
+ },
+ "openapi_source_binding_source_scope_id_idx": {
+ "name": "openapi_source_binding_source_scope_id_idx",
+ "columns": [
+ "source_scope_id"
+ ],
+ "isUnique": false
+ },
+ "openapi_source_binding_target_scope_id_idx": {
+ "name": "openapi_source_binding_target_scope_id_idx",
+ "columns": [
+ "target_scope_id"
+ ],
+ "isUnique": false
+ },
+ "openapi_source_binding_slot_idx": {
+ "name": "openapi_source_binding_slot_idx",
+ "columns": [
+ "slot"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "secret": {
+ "name": "secret",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owned_by_connection_id": {
+ "name": "owned_by_connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "secret_scope_id_idx": {
+ "name": "secret_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "secret_provider_idx": {
+ "name": "secret_provider_idx",
+ "columns": [
+ "provider"
+ ],
+ "isUnique": false
+ },
+ "secret_owned_by_connection_id_idx": {
+ "name": "secret_owned_by_connection_id_idx",
+ "columns": [
+ "owned_by_connection_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "secret_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "secret_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "source": {
+ "name": "source",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "can_remove": {
+ "name": "can_remove",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "can_refresh": {
+ "name": "can_refresh",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "can_edit": {
+ "name": "can_edit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "source_scope_id_idx": {
+ "name": "source_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "source_plugin_id_idx": {
+ "name": "source_plugin_id_idx",
+ "columns": [
+ "plugin_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "source_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "source_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "tool": {
+ "name": "tool",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope_id": {
+ "name": "scope_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "input_schema": {
+ "name": "input_schema",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "output_schema": {
+ "name": "output_schema",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "tool_scope_id_idx": {
+ "name": "tool_scope_id_idx",
+ "columns": [
+ "scope_id"
+ ],
+ "isUnique": false
+ },
+ "tool_source_id_idx": {
+ "name": "tool_source_id_idx",
+ "columns": [
+ "source_id"
+ ],
+ "isUnique": false
+ },
+ "tool_plugin_id_idx": {
+ "name": "tool_plugin_id_idx",
+ "columns": [
+ "plugin_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "tool_scope_id_id_pk": {
+ "columns": [
+ "scope_id",
+ "id"
+ ],
+ "name": "tool_scope_id_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/local/drizzle/meta/_journal.json b/apps/local/drizzle/meta/_journal.json
index 7cbca7936..361b9ec71 100644
--- a/apps/local/drizzle/meta/_journal.json
+++ b/apps/local/drizzle/meta/_journal.json
@@ -29,6 +29,13 @@
"when": 1776976132767,
"tag": "0003_little_silk_fever",
"breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "6",
+ "when": 1777044230384,
+ "tag": "0004_fancy_red_wolf",
+ "breakpoints": true
}
]
-}
+}
\ No newline at end of file
diff --git a/apps/local/src/routeTree.gen.ts b/apps/local/src/routeTree.gen.ts
index b068e7fb3..e98f21d74 100644
--- a/apps/local/src/routeTree.gen.ts
+++ b/apps/local/src/routeTree.gen.ts
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as ToolsRouteImport } from './routes/tools'
import { Route as SecretsRouteImport } from './routes/secrets'
+import { Route as RunsRouteImport } from './routes/runs'
import { Route as ConnectionsRouteImport } from './routes/connections'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SourcesNamespaceRouteImport } from './routes/sources.$namespace'
@@ -21,6 +22,11 @@ const ToolsRoute = ToolsRouteImport.update({
path: '/tools',
getParentRoute: () => rootRouteImport,
} as any)
+const RunsRoute = RunsRouteImport.update({
+ id: '/runs',
+ path: '/runs',
+ getParentRoute: () => rootRouteImport,
+} as any)
const SecretsRoute = SecretsRouteImport.update({
id: '/secrets',
path: '/secrets',
@@ -50,6 +56,7 @@ const SourcesAddPluginKeyRoute = SourcesAddPluginKeyRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/connections': typeof ConnectionsRoute
+ '/runs': typeof RunsRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
@@ -58,6 +65,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/connections': typeof ConnectionsRoute
+ '/runs': typeof RunsRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
@@ -67,6 +75,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/connections': typeof ConnectionsRoute
+ '/runs': typeof RunsRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
@@ -77,6 +86,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/connections'
+ | '/runs'
| '/secrets'
| '/tools'
| '/sources/$namespace'
@@ -85,6 +95,7 @@ export interface FileRouteTypes {
to:
| '/'
| '/connections'
+ | '/runs'
| '/secrets'
| '/tools'
| '/sources/$namespace'
@@ -93,6 +104,7 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/connections'
+ | '/runs'
| '/secrets'
| '/tools'
| '/sources/$namespace'
@@ -102,6 +114,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ConnectionsRoute: typeof ConnectionsRoute
+ RunsRoute: typeof RunsRoute
SecretsRoute: typeof SecretsRoute
ToolsRoute: typeof ToolsRoute
SourcesNamespaceRoute: typeof SourcesNamespaceRoute
@@ -124,6 +137,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SecretsRouteImport
parentRoute: typeof rootRouteImport
}
+ '/runs': {
+ id: '/runs'
+ path: '/runs'
+ fullPath: '/runs'
+ preLoaderRoute: typeof RunsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/connections': {
id: '/connections'
path: '/connections'
@@ -158,6 +178,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ConnectionsRoute: ConnectionsRoute,
+ RunsRoute: RunsRoute,
SecretsRoute: SecretsRoute,
ToolsRoute: ToolsRoute,
SourcesNamespaceRoute: SourcesNamespaceRoute,
diff --git a/apps/local/src/routes/runs.tsx b/apps/local/src/routes/runs.tsx
new file mode 100644
index 000000000..175234c0e
--- /dev/null
+++ b/apps/local/src/routes/runs.tsx
@@ -0,0 +1,24 @@
+import { Schema } from "effect";
+import { createFileRoute } from "@tanstack/react-router";
+import { RunsPage, type RunsSearch } from "@executor/react/pages/runs";
+
+const RunsSearchSchema = Schema.standardSchemaV1(
+ Schema.Struct({
+ executionId: Schema.optional(Schema.String),
+ status: Schema.optional(Schema.String),
+ trigger: Schema.optional(Schema.String),
+ tool: Schema.optional(Schema.String),
+ range: Schema.optional(Schema.String),
+ from: Schema.optional(Schema.String),
+ to: Schema.optional(Schema.String),
+ code: Schema.optional(Schema.String),
+ live: Schema.optional(Schema.String),
+ sort: Schema.optional(Schema.String),
+ elicitation: Schema.optional(Schema.String),
+ }),
+);
+
+export const Route = createFileRoute("/runs")({
+ validateSearch: RunsSearchSchema,
+ component: () => ,
+});
diff --git a/apps/local/src/server/executor-schema.ts b/apps/local/src/server/executor-schema.ts
index 697159a8a..df25c3e77 100644
--- a/apps/local/src/server/executor-schema.ts
+++ b/apps/local/src/server/executor-schema.ts
@@ -225,3 +225,70 @@ export const graphql_operation = sqliteTable("graphql_operation", {
index("graphql_operation_source_id_idx").on(table.source_id),
]);
+// Execution history — one row per engine.execute() / executeWithPause()
+// call. `scope_id` is the innermost executor scope that owned the run;
+// the scoped adapter filters these on every list query. JSON-bearing
+// columns (result/error/logs/trigger-meta) are text blobs; the SDK never
+// parses them server-side.
+export const execution = sqliteTable("execution", {
+ id: text('id').notNull(),
+ scope_id: text('scope_id').notNull(),
+ status: text('status').notNull(),
+ code: text('code').notNull(),
+ result_json: text('result_json'),
+ error_text: text('error_text'),
+ logs_json: text('logs_json'),
+ started_at: integer('started_at'),
+ completed_at: integer('completed_at'),
+ trigger_kind: text('trigger_kind'),
+ trigger_meta_json: text('trigger_meta_json'),
+ tool_call_count: integer('tool_call_count').default(0).notNull(),
+ created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
+ updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()
+}, (table) => [
+ primaryKey({ columns: [table.scope_id, table.id] }),
+ index("execution_scope_id_idx").on(table.scope_id),
+ index("execution_status_idx").on(table.status),
+ index("execution_trigger_kind_idx").on(table.trigger_kind),
+ index("execution_created_at_idx").on(table.created_at),
+]);
+
+// Per-execution interaction rows — elicitation requests + their
+// resolutions. Not scope-owned; tenant isolation flows through the
+// parent execution.
+export const execution_interaction = sqliteTable("execution_interaction", {
+ id: text('id').primaryKey(),
+ execution_id: text('execution_id').notNull(),
+ status: text('status').notNull(),
+ kind: text('kind').notNull(),
+ purpose: text('purpose'),
+ payload_json: text('payload_json'),
+ response_json: text('response_json'),
+ response_private_json: text('response_private_json'),
+ created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
+ updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()
+}, (table) => [
+ index("execution_interaction_execution_id_idx").on(table.execution_id),
+ index("execution_interaction_status_idx").on(table.status),
+]);
+
+// Per-execution tool-call rows — one per executor.tools.invoke call
+// inside the sandboxed execution. Powers the runs UI's tool-call
+// timeline + facet list.
+export const execution_tool_call = sqliteTable("execution_tool_call", {
+ id: text('id').primaryKey(),
+ execution_id: text('execution_id').notNull(),
+ status: text('status').notNull(),
+ tool_path: text('tool_path').notNull(),
+ namespace: text('namespace'),
+ args_json: text('args_json'),
+ result_json: text('result_json'),
+ error_text: text('error_text'),
+ started_at: integer('started_at').notNull(),
+ completed_at: integer('completed_at'),
+ duration_ms: integer('duration_ms')
+}, (table) => [
+ index("execution_tool_call_execution_id_idx").on(table.execution_id),
+ index("execution_tool_call_tool_path_idx").on(table.tool_path),
+ index("execution_tool_call_namespace_idx").on(table.namespace),
+]);
diff --git a/apps/local/src/web/shell.tsx b/apps/local/src/web/shell.tsx
index 879210adb..b1baab69c 100644
--- a/apps/local/src/web/shell.tsx
+++ b/apps/local/src/web/shell.tsx
@@ -305,6 +305,7 @@ function SidebarContent(props: {
}) {
const isHome = props.pathname === "/";
const isSecrets = props.pathname === "/secrets";
+ const isRuns = props.pathname === "/runs";
const isConnections = props.pathname === "/connections";
return (
@@ -322,6 +323,7 @@ function SidebarContent(props: {
+
{/* Sources list */}
diff --git a/bun.lock b/bun.lock
index 89db25bd4..35237c272 100644
--- a/bun.lock
+++ b/bun.lock
@@ -766,6 +766,7 @@
"version": "1.4.3",
"dependencies": {
"@base-ui/react": "^1.3.0",
+ "@date-fns/utc": "^2.1.0",
"@effect-atom/atom": "^0.5.0",
"@effect-atom/atom-react": "^0.5.0",
"@effect/platform": "catalog:",
@@ -775,10 +776,12 @@
"@lobehub/icons": "^5.4.0",
"@shikijs/langs": "^4.0.2",
"@shikijs/themes": "^4.0.2",
+ "@tanstack/react-query": "^5.62.12",
"@tanstack/react-router": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "date-fns": "^3.6.0",
"effect": "catalog:",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
@@ -787,6 +790,7 @@
"react": "catalog:",
"react-day-picker": "^9.14.0",
"react-hook-form": "^7.72.0",
+ "react-hotkeys-hook": "^5.2.4",
"react-resizable-panels": "^4",
"recharts": "3.8.0",
"shiki": "^4.0.2",
@@ -1061,6 +1065,8 @@
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
+ "@date-fns/utc": ["@date-fns/utc@2.1.1", "", {}, "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA=="],
+
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
@@ -2819,7 +2825,7 @@
"dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
- "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
+ "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
"date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
@@ -4753,6 +4759,8 @@
"@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+ "@base-ui/react/date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
+
"@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
"@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
@@ -5245,6 +5253,8 @@
"rc-menu/@rc-component/trigger": ["@rc-component/trigger@2.3.1", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="],
+ "react-day-picker/date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
+
"react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
diff --git a/packages/core/api/src/executions/api.ts b/packages/core/api/src/executions/api.ts
index 12b557919..2bc9f654f 100644
--- a/packages/core/api/src/executions/api.ts
+++ b/packages/core/api/src/executions/api.ts
@@ -11,6 +11,144 @@ const ExecuteRequest = Schema.Struct({
code: Schema.String,
});
+/**
+ * Optional header naming the surface that triggered this execution —
+ * `"cli"`, `"http"`, `"mcp"`, etc. Persisted on the execution row so
+ * the runs UI can facet by trigger kind. Defaults to `"http"` when
+ * absent.
+ */
+const ExecuteHeaders = Schema.Struct({
+ "x-executor-trigger": Schema.optional(Schema.String),
+});
+
+const ExecutionStatusLiteral = Schema.Literal(
+ "pending",
+ "running",
+ "waiting_for_interaction",
+ "completed",
+ "failed",
+ "cancelled",
+);
+
+const ExecutionRecord = Schema.Struct({
+ id: Schema.String,
+ scopeId: Schema.String,
+ status: ExecutionStatusLiteral,
+ code: Schema.String,
+ resultJson: Schema.NullOr(Schema.String),
+ errorText: Schema.NullOr(Schema.String),
+ logsJson: Schema.NullOr(Schema.String),
+ startedAt: Schema.NullOr(Schema.Number),
+ completedAt: Schema.NullOr(Schema.Number),
+ triggerKind: Schema.NullOr(Schema.String),
+ triggerMetaJson: Schema.NullOr(Schema.String),
+ toolCallCount: Schema.Number,
+ createdAt: Schema.Number,
+ updatedAt: Schema.Number,
+});
+
+const ExecutionInteractionRecord = Schema.Struct({
+ id: Schema.String,
+ executionId: Schema.String,
+ status: Schema.Literal("pending", "resolved", "cancelled"),
+ kind: Schema.String,
+ purpose: Schema.NullOr(Schema.String),
+ payloadJson: Schema.NullOr(Schema.String),
+ responseJson: Schema.NullOr(Schema.String),
+ responsePrivateJson: Schema.NullOr(Schema.String),
+ createdAt: Schema.Number,
+ updatedAt: Schema.Number,
+});
+
+const ExecutionToolCallRecord = Schema.Struct({
+ id: Schema.String,
+ executionId: Schema.String,
+ status: Schema.Literal("running", "completed", "failed"),
+ toolPath: Schema.String,
+ namespace: Schema.NullOr(Schema.String),
+ argsJson: Schema.NullOr(Schema.String),
+ resultJson: Schema.NullOr(Schema.String),
+ errorText: Schema.NullOr(Schema.String),
+ startedAt: Schema.Number,
+ completedAt: Schema.NullOr(Schema.Number),
+ durationMs: Schema.NullOr(Schema.Number),
+});
+
+const ExecutionListItemResponse = Schema.Struct({
+ execution: ExecutionRecord,
+ pendingInteraction: Schema.NullOr(ExecutionInteractionRecord),
+});
+
+const ExecutionStatusCount = Schema.Struct({
+ status: ExecutionStatusLiteral,
+ count: Schema.Number,
+});
+const ExecutionTriggerCount = Schema.Struct({
+ triggerKind: Schema.NullOr(Schema.String),
+ count: Schema.Number,
+});
+const ExecutionToolFacet = Schema.Struct({
+ toolPath: Schema.String,
+ count: Schema.Number,
+});
+const ExecutionChartBucket = Schema.Struct({
+ bucketStart: Schema.Number,
+ counts: Schema.Record({ key: Schema.String, value: Schema.Number }),
+});
+const ExecutionListMeta = Schema.Struct({
+ totalRowCount: Schema.Number,
+ filterRowCount: Schema.Number,
+ statusCounts: Schema.Array(ExecutionStatusCount),
+ triggerCounts: Schema.Array(ExecutionTriggerCount),
+ toolFacets: Schema.Array(ExecutionToolFacet),
+ interactionCounts: Schema.Struct({
+ withElicitation: Schema.Number,
+ withoutElicitation: Schema.Number,
+ }),
+ chartBucketMs: Schema.Number,
+ chartData: Schema.Array(ExecutionChartBucket),
+});
+
+const ListExecutionsResponse = Schema.Struct({
+ executions: Schema.Array(ExecutionListItemResponse),
+ nextCursor: Schema.optional(Schema.String),
+ meta: Schema.optional(ExecutionListMeta),
+});
+
+const GetExecutionResponse = Schema.Struct({
+ execution: ExecutionRecord,
+ pendingInteraction: Schema.NullOr(ExecutionInteractionRecord),
+});
+
+const ListToolCallsResponse = Schema.Struct({
+ toolCalls: Schema.Array(ExecutionToolCallRecord),
+});
+
+/**
+ * Query-string filters for `GET /executions`. Every param is optional
+ * and arrives as a plain string so the client side doesn't need to
+ * know about Effect Schema. The handler normalizes CSV fields and
+ * validates enums.
+ */
+const ListExecutionsParams = Schema.Struct({
+ limit: Schema.optional(Schema.NumberFromString),
+ cursor: Schema.optional(Schema.String),
+ /** CSV of ExecutionStatus. Invalid values are dropped. */
+ status: Schema.optional(Schema.String),
+ /** CSV of trigger kinds. Use "unknown" to match rows with null. */
+ trigger: Schema.optional(Schema.String),
+ /** CSV of tool paths / globs (`github.*`). */
+ tool: Schema.optional(Schema.String),
+ from: Schema.optional(Schema.NumberFromString),
+ to: Schema.optional(Schema.NumberFromString),
+ after: Schema.optional(Schema.String),
+ code: Schema.optional(Schema.String),
+ /** `
,` — e.g. `createdAt,desc`. */
+ sort: Schema.optional(Schema.String),
+ /** `"true"` / `"false"` to filter to runs that did or didn't elicit. */
+ elicitation: Schema.optional(Schema.String),
+});
+
const CompletedResult = Schema.Struct({
status: Schema.Literal("completed"),
text: Schema.String,
@@ -55,6 +193,7 @@ export class ExecutionsApi extends HttpApiGroup.make("executions")
.add(
HttpApiEndpoint.post("execute")`/executions`
.setPayload(ExecuteRequest)
+ .setHeaders(ExecuteHeaders)
.addSuccess(ExecuteResponse),
)
.add(
@@ -63,4 +202,21 @@ export class ExecutionsApi extends HttpApiGroup.make("executions")
.addSuccess(ResumeResponse)
.addError(ExecutionNotFoundError),
)
+ .add(
+ HttpApiEndpoint.get("list")`/executions`
+ .setUrlParams(ListExecutionsParams)
+ .addSuccess(ListExecutionsResponse),
+ )
+ .add(
+ HttpApiEndpoint.get("get")`/executions/${executionIdParam}`
+ .addSuccess(GetExecutionResponse)
+ .addError(ExecutionNotFoundError),
+ )
+ .add(
+ HttpApiEndpoint.get(
+ "listToolCalls",
+ )`/executions/${executionIdParam}/tool-calls`
+ .addSuccess(ListToolCallsResponse)
+ .addError(ExecutionNotFoundError),
+ )
.addError(InternalError) {}
diff --git a/packages/core/api/src/handlers/executions.ts b/packages/core/api/src/handlers/executions.ts
index 3eedd3e65..3d9d27aa6 100644
--- a/packages/core/api/src/handlers/executions.ts
+++ b/packages/core/api/src/handlers/executions.ts
@@ -3,15 +3,62 @@ import { Effect } from "effect";
import { ExecutorApi } from "../api";
import { formatExecuteResult, formatPausedExecution } from "@executor/execution";
-import { ExecutionEngineService } from "../services";
+import {
+ EXECUTION_STATUS_KEYS,
+ ExecutionId,
+ type ExecutionSort,
+ type ExecutionStatus,
+} from "@executor/sdk";
+import { ExecutionEngineService, ExecutorService } from "../services";
import { capture, captureEngineError } from "@executor/api";
+// ---------------------------------------------------------------------------
+// Query-string helpers
+// ---------------------------------------------------------------------------
+
+const STATUS_SET = new Set(EXECUTION_STATUS_KEYS);
+const SORT_FIELDS = new Set(["createdAt", "durationMs"] as const);
+const SORT_DIRECTIONS = new Set(["asc", "desc"] as const);
+
+const splitCsv = (value: string | undefined): string[] =>
+ value
+ ? value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
+ : [];
+
+const parseSortParam = (raw: string | undefined): ExecutionSort | undefined => {
+ if (!raw) return undefined;
+ const [rawField, rawDirection] = raw.split(",").map((s) => s.trim());
+ if (!rawField || !rawDirection) return undefined;
+ if (!SORT_FIELDS.has(rawField as (typeof SORT_FIELDS extends Set ? T : never)))
+ return undefined;
+ if (!SORT_DIRECTIONS.has(rawDirection as "asc" | "desc")) return undefined;
+ return {
+ field: rawField as ExecutionSort["field"],
+ direction: rawDirection as ExecutionSort["direction"],
+ };
+};
+
+const parseElicitationParam = (raw: string | undefined): boolean | undefined => {
+ if (raw === "true") return true;
+ if (raw === "false") return false;
+ return undefined;
+};
+
+// ---------------------------------------------------------------------------
+// Handlers
+// ---------------------------------------------------------------------------
+
export const ExecutionsHandlers = HttpApiBuilder.group(ExecutorApi, "executions", (handlers) =>
handlers
- .handle("execute", ({ payload }) =>
+ .handle("execute", ({ payload, headers }) =>
capture(Effect.gen(function* () {
const engine = yield* ExecutionEngineService;
- const outcome = yield* captureEngineError(engine.executeWithPause(payload.code));
+ const triggerKind = headers["x-executor-trigger"] ?? "http";
+ const outcome = yield* captureEngineError(
+ engine.executeWithPause(payload.code, {
+ trigger: { kind: triggerKind },
+ }),
+ );
if (outcome.status === "completed") {
const formatted = formatExecuteResult(outcome.result);
@@ -64,5 +111,104 @@ export const ExecutionsHandlers = HttpApiBuilder.group(ExecutorApi, "executions"
isError: false,
};
})),
+ )
+ .handle("list", ({ urlParams }) =>
+ capture(Effect.gen(function* () {
+ const executor = yield* ExecutorService;
+ // Executions are scoped to the innermost scope of the current
+ // executor stack — same rule the engine uses when writing the row.
+ const scopeId = executor.scopes[0]!.id;
+
+ const statusFilter = splitCsv(urlParams.status).filter(
+ (v): v is ExecutionStatus => STATUS_SET.has(v),
+ );
+ const triggerFilter = splitCsv(urlParams.trigger);
+ const toolPathFilter = splitCsv(urlParams.tool);
+ const includeMeta =
+ urlParams.cursor === undefined && urlParams.after === undefined;
+
+ const result = yield* executor.executions.list(scopeId, {
+ limit: urlParams.limit,
+ cursor: urlParams.cursor,
+ statusFilter: statusFilter.length > 0 ? statusFilter : undefined,
+ triggerFilter: triggerFilter.length > 0 ? triggerFilter : undefined,
+ toolPathFilter: toolPathFilter.length > 0 ? toolPathFilter : undefined,
+ after: urlParams.after,
+ timeRange:
+ urlParams.from !== undefined || urlParams.to !== undefined
+ ? { from: urlParams.from, to: urlParams.to }
+ : undefined,
+ codeQuery: urlParams.code,
+ sort: parseSortParam(urlParams.sort),
+ hadElicitation: parseElicitationParam(urlParams.elicitation),
+ includeMeta,
+ });
+
+ return {
+ executions: result.executions.map((item) => ({
+ execution: {
+ ...item.execution,
+ createdAt: item.execution.createdAt.getTime(),
+ updatedAt: item.execution.updatedAt.getTime(),
+ },
+ pendingInteraction: item.pendingInteraction
+ ? {
+ ...item.pendingInteraction,
+ createdAt: item.pendingInteraction.createdAt.getTime(),
+ updatedAt: item.pendingInteraction.updatedAt.getTime(),
+ }
+ : null,
+ })),
+ ...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
+ ...(result.meta ? { meta: result.meta } : {}),
+ };
+ })),
+ )
+ .handle("get", ({ path }) =>
+ capture(Effect.gen(function* () {
+ const executor = yield* ExecutorService;
+ const detail = yield* executor.executions.get(
+ ExecutionId.make(path.executionId),
+ );
+ if (!detail) {
+ return yield* Effect.fail({
+ _tag: "ExecutionNotFoundError" as const,
+ executionId: path.executionId,
+ });
+ }
+ return {
+ execution: {
+ ...detail.execution,
+ createdAt: detail.execution.createdAt.getTime(),
+ updatedAt: detail.execution.updatedAt.getTime(),
+ },
+ pendingInteraction: detail.pendingInteraction
+ ? {
+ ...detail.pendingInteraction,
+ createdAt: detail.pendingInteraction.createdAt.getTime(),
+ updatedAt: detail.pendingInteraction.updatedAt.getTime(),
+ }
+ : null,
+ };
+ })),
+ )
+ .handle("listToolCalls", ({ path }) =>
+ capture(Effect.gen(function* () {
+ const executor = yield* ExecutorService;
+ // Guard so missing executions 404 instead of returning `[]`.
+ const detail = yield* executor.executions.get(
+ ExecutionId.make(path.executionId),
+ );
+ if (!detail) {
+ return yield* Effect.fail({
+ _tag: "ExecutionNotFoundError" as const,
+ executionId: path.executionId,
+ });
+ }
+ const toolCalls = yield* executor.executions.listToolCalls(
+ ExecutionId.make(path.executionId),
+ );
+ return { toolCalls };
+ })),
),
);
diff --git a/packages/core/execution/src/engine-persistence.test.ts b/packages/core/execution/src/engine-persistence.test.ts
new file mode 100644
index 000000000..3b7b697c8
--- /dev/null
+++ b/packages/core/execution/src/engine-persistence.test.ts
@@ -0,0 +1,270 @@
+import { describe, expect, it } from "@effect/vitest";
+import { Effect } from "effect";
+
+import {
+ createExecutor,
+ definePlugin,
+ ElicitationResponse,
+ ExecutionId,
+ makeTestConfig,
+ type ElicitationHandler,
+} from "@executor/sdk";
+import { CodeExecutionError } from "@executor/codemode-core";
+import type {
+ CodeExecutor,
+ ExecuteResult,
+ SandboxToolInvoker,
+} from "@executor/codemode-core";
+
+import { createExecutionEngine } from "./engine";
+
+// ---------------------------------------------------------------------------
+// Stub CodeExecutor that drives the invoker + elicitation handler from a
+// fixed script. Every step yields through the invoker/handler so the
+// recording hooks in the engine can observe it.
+// ---------------------------------------------------------------------------
+
+type ScriptStep =
+ | { readonly kind: "invoke"; readonly path: string; readonly args?: unknown }
+ | { readonly kind: "elicit"; readonly message: string };
+
+const makeScriptedExecutor = (
+ steps: readonly ScriptStep[],
+ result: ExecuteResult,
+): CodeExecutor => ({
+ execute: (_code, invoker) =>
+ Effect.gen(function* () {
+ for (const step of steps) {
+ if (step.kind === "invoke") {
+ yield* invoker
+ .invoke({ path: step.path, args: step.args })
+ .pipe(Effect.ignore);
+ }
+ }
+ return result;
+ }),
+});
+
+// ---------------------------------------------------------------------------
+// Test plugin — one tool that echoes `{ ok: true, echo: args }`.
+// ---------------------------------------------------------------------------
+
+const echoPlugin = definePlugin(() => ({
+ id: "echo-plugin" as const,
+ storage: () => ({}),
+ staticSources: () => [
+ {
+ id: "echo",
+ kind: "in-memory",
+ name: "Echo",
+ tools: [
+ {
+ name: "ping",
+ description: "Echo back the input",
+ inputSchema: {
+ type: "object",
+ properties: { message: { type: "string" } },
+ additionalProperties: true,
+ } as const,
+ handler: ({ args }: { args: unknown }) =>
+ Effect.succeed({ ok: true, echo: args }),
+ },
+ ],
+ },
+ ],
+}));
+
+const makeEngine = (codeExecutor: CodeExecutor) =>
+ Effect.gen(function* () {
+ const executor = yield* createExecutor(
+ makeTestConfig({ plugins: [echoPlugin()] as const }),
+ );
+ const engine = createExecutionEngine({ executor, codeExecutor });
+ return { executor, engine };
+ });
+
+const acceptAll: ElicitationHandler = () =>
+ Effect.succeed(new ElicitationResponse({ action: "accept" }));
+
+describe("engine persistence", () => {
+ it.effect("execute() records a completed run + every tool call", () =>
+ Effect.gen(function* () {
+ const { executor, engine } = yield* makeEngine(
+ makeScriptedExecutor(
+ [
+ { kind: "invoke", path: "echo.ping", args: { message: "hi" } },
+ { kind: "invoke", path: "echo.ping", args: { message: "bye" } },
+ ],
+ { result: { ok: true }, logs: ["[log] hello"] },
+ ),
+ );
+
+ yield* engine.execute("await tools.echo.ping({message:'hi'})", {
+ onElicitation: acceptAll,
+ trigger: { kind: "test" },
+ });
+
+ // The scoped test executor uses the "test-scope" id.
+ const result = yield* executor.executions.list(
+ executor.scopes[0]!.id,
+ {},
+ );
+ expect(result.executions).toHaveLength(1);
+ const { execution } = result.executions[0]!;
+ expect(execution.status).toBe("completed");
+ expect(execution.triggerKind).toBe("test");
+ expect(execution.toolCallCount).toBe(2);
+ expect(execution.resultJson).toBe('{"ok":true}');
+ expect(execution.logsJson).toBe('["[log] hello"]');
+
+ const calls = yield* executor.executions.listToolCalls(execution.id);
+ expect(calls).toHaveLength(2);
+ expect(calls.map((c) => c.toolPath)).toEqual(["echo.ping", "echo.ping"]);
+ expect(calls.every((c) => c.status === "completed")).toBe(true);
+ expect(calls.every((c) => typeof c.durationMs === "number")).toBe(true);
+ }),
+ );
+
+ it.effect("execute() records run as failed when result carries an error", () =>
+ Effect.gen(function* () {
+ const { executor, engine } = yield* makeEngine(
+ makeScriptedExecutor(
+ [],
+ { result: null, error: "boom", logs: [] },
+ ),
+ );
+
+ yield* engine.execute("throw new Error('boom')", {
+ onElicitation: acceptAll,
+ });
+
+ const { executions } = yield* executor.executions.list(
+ executor.scopes[0]!.id,
+ {},
+ );
+ expect(executions).toHaveLength(1);
+ expect(executions[0]!.execution.status).toBe("failed");
+ expect(executions[0]!.execution.errorText).toBe("boom");
+ }),
+ );
+
+ it.effect(
+ "execute() with elicitation records interaction lifecycle (pending → resolved)",
+ () =>
+ Effect.gen(function* () {
+ const scriptedInvoker: CodeExecutor = {
+ execute: (_code, invoker: SandboxToolInvoker) =>
+ Effect.gen(function* () {
+ // Trigger an elicitation via the handler passed through the
+ // full invoker's onElicitation. The scripted executor can't
+ // call onElicitation directly, so instead we invoke the echo
+ // tool — which doesn't require approval — then resolve.
+ yield* invoker
+ .invoke({ path: "echo.ping", args: {} })
+ .pipe(Effect.ignore);
+ return { result: "done" } satisfies ExecuteResult;
+ }),
+ };
+ const { executor, engine } = yield* makeEngine(scriptedInvoker);
+
+ // Wire a handler that will be observed as a recordInteraction +
+ // resolveInteraction pair if anything calls it — here nothing
+ // does, so we just verify the happy path passes cleanly.
+ yield* engine.execute("noop", {
+ onElicitation: () =>
+ Effect.succeed(new ElicitationResponse({ action: "accept" })),
+ });
+
+ const { executions } = yield* executor.executions.list(
+ executor.scopes[0]!.id,
+ { includeMeta: true },
+ );
+ expect(executions).toHaveLength(1);
+ expect(executions[0]!.execution.toolCallCount).toBe(1);
+ }),
+ );
+
+ it.effect("trigger metadata is persisted on the execution row", () =>
+ Effect.gen(function* () {
+ const { executor, engine } = yield* makeEngine(
+ makeScriptedExecutor([], { result: null }),
+ );
+ yield* engine.execute("const x = 1", {
+ onElicitation: acceptAll,
+ trigger: { kind: "mcp", meta: { sessionId: "abc-123" } },
+ });
+
+ const { executions } = yield* executor.executions.list(
+ executor.scopes[0]!.id,
+ {},
+ );
+ expect(executions[0]!.execution.triggerKind).toBe("mcp");
+ expect(executions[0]!.execution.triggerMetaJson).toBe(
+ '{"sessionId":"abc-123"}',
+ );
+ }),
+ );
+
+ it.effect("tool call failure records the failed status + error text", () =>
+ Effect.gen(function* () {
+ const failingExecutor: CodeExecutor = {
+ execute: (_code, invoker) =>
+ Effect.gen(function* () {
+ const ran = yield* invoker
+ .invoke({ path: "echo.ping", args: { willFail: true } })
+ .pipe(Effect.either);
+ return {
+ result: ran._tag === "Right" ? ran.right : null,
+ error: ran._tag === "Left" ? "tool failed" : undefined,
+ } satisfies ExecuteResult;
+ }),
+ };
+
+ const failingPlugin = definePlugin(() => ({
+ id: "failing-plugin" as const,
+ storage: () => ({}),
+ staticSources: () => [
+ {
+ id: "echo",
+ kind: "in-memory",
+ name: "Echo",
+ tools: [
+ {
+ name: "ping",
+ description: "Always fails",
+ inputSchema: {
+ type: "object",
+ properties: {},
+ additionalProperties: true,
+ } as const,
+ handler: () => Effect.fail(new Error("tool blew up")),
+ },
+ ],
+ },
+ ],
+ }));
+
+ const executor = yield* createExecutor(
+ makeTestConfig({ plugins: [failingPlugin()] as const }),
+ );
+ const engine = createExecutionEngine({
+ executor,
+ codeExecutor: failingExecutor,
+ });
+
+ yield* engine.execute("await tools.echo.ping({})", {
+ onElicitation: acceptAll,
+ });
+
+ const { executions } = yield* executor.executions.list(
+ executor.scopes[0]!.id,
+ {},
+ );
+ const executionId = ExecutionId.make(executions[0]!.execution.id);
+ const calls = yield* executor.executions.listToolCalls(executionId);
+ expect(calls).toHaveLength(1);
+ expect(calls[0]!.status).toBe("failed");
+ expect(calls[0]!.errorText).toBeTruthy();
+ }),
+ );
+});
diff --git a/packages/core/execution/src/engine.ts b/packages/core/execution/src/engine.ts
index ab6288c32..1bb30c27b 100644
--- a/packages/core/execution/src/engine.ts
+++ b/packages/core/execution/src/engine.ts
@@ -1,12 +1,15 @@
-import { Deferred, Effect, Fiber, Ref } from "effect";
+import { Cause as EffectCause, Deferred, Effect, Fiber, Ref } from "effect";
import type * as Cause from "effect/Cause";
-import type {
- Executor,
- InvokeOptions,
- ElicitationResponse,
- ElicitationHandler,
- ElicitationContext,
+import {
+ ExecutionId,
+ ExecutionInteractionId,
+ ExecutionToolCallId,
+ type ElicitationContext,
+ type ElicitationHandler,
+ type ElicitationResponse,
+ type Executor,
+ type InvokeOptions,
} from "@executor/sdk";
import { CodeExecutionError } from "@executor/codemode-core";
import type { CodeExecutor, ExecuteResult, SandboxToolInvoker } from "@executor/codemode-core";
@@ -40,11 +43,19 @@ export type PausedExecution = {
readonly elicitationContext: ElicitationContext;
};
+/** Trigger metadata — what surface started this run. Persisted on the
+ * execution row; filter facets in the runs UI read from it. */
+export type ExecutionTrigger = {
+ readonly kind: string;
+ readonly meta?: Record;
+};
+
/** Internal representation with Effect runtime state for pause/resume. */
type InternalPausedExecution = PausedExecution & {
readonly response: Deferred.Deferred;
readonly fiber: Fiber.Fiber;
readonly pauseSignalRef: Ref.Ref>>;
+ readonly interactionId: ExecutionInteractionId;
};
export type ResumeResponse = {
@@ -136,6 +147,56 @@ export const formatPausedExecution = (
};
};
+// ---------------------------------------------------------------------------
+// Recording helpers — serialize payloads for the execution_* tables
+// without throwing on cyclic/unserializable values.
+// ---------------------------------------------------------------------------
+
+/** Best-effort wrapper for execution-history writes. Absorbs both typed
+ * failures AND defects (e.g. a backend adapter that throws synchronously
+ * for an unknown model before the app-level Drizzle schema has been
+ * migrated), so bookkeeping can never fail a tool call or a user
+ * execution. A caller that wants to know about these errors should
+ * inspect Axiom spans or add their own tracer. */
+const silent = (effect: Effect.Effect): Effect.Effect =>
+ effect.pipe(Effect.catchAllCause(() => Effect.void));
+
+const safeStringify = (value: unknown): string => {
+ try {
+ return JSON.stringify(value);
+ } catch {
+ return String(value);
+ }
+};
+
+const formatErrorMessage = (err: unknown): string => {
+ if (err instanceof Error) return err.message;
+ if (typeof err === "string") return err;
+ if (
+ typeof err === "object" &&
+ err !== null &&
+ "message" in err &&
+ typeof (err as { message: unknown }).message === "string"
+ ) {
+ return (err as { message: string }).message;
+ }
+ return safeStringify(err);
+};
+
+const formatCauseMessage = (cause: Cause.Cause): string =>
+ formatErrorMessage(EffectCause.squash(cause));
+
+const serializeElicitationRequest = (ctx: ElicitationContext) => {
+ const req = ctx.request;
+ return req._tag === "UrlElicitation"
+ ? { kind: "url", message: req.message, url: req.url }
+ : {
+ kind: "form",
+ message: req.message,
+ requestedSchema: req.requestedSchema,
+ };
+};
+
// ---------------------------------------------------------------------------
// Full invoker (base + discover + describe)
// ---------------------------------------------------------------------------
@@ -286,7 +347,10 @@ export type ExecutionEngine
*/
readonly execute: (
code: string,
- options: { readonly onElicitation: ElicitationHandler },
+ options: {
+ readonly onElicitation: ElicitationHandler;
+ readonly trigger?: ExecutionTrigger;
+ },
) => Effect.Effect;
/**
@@ -294,7 +358,10 @@ export type ExecutionEngine
* Use this when the host doesn't support inline elicitation.
* Returns either a completed result or a paused execution that can be resumed.
*/
- readonly executeWithPause: (code: string) => Effect.Effect;
+ readonly executeWithPause: (
+ code: string,
+ options?: { readonly trigger?: ExecutionTrigger },
+ ) => Effect.Effect;
/**
* Resume a paused execution. Returns a completed result, a new pause, or
@@ -318,19 +385,136 @@ export const createExecutionEngine = <
): ExecutionEngine => {
const { executor, codeExecutor } = config;
const pausedExecutions = new Map>();
- let nextId = 0;
+ /** Tracks the running tool-call counter per active execution. Carries
+ * across pause/resume: the fiber keeps the same counter ref even
+ * though the Ref itself lives in the engine closure. */
+ const toolCallCounters = new Map>();
+
+ const newExecutionId = (): ExecutionId =>
+ ExecutionId.make(crypto.randomUUID());
+ const newInteractionId = (): ExecutionInteractionId =>
+ ExecutionInteractionId.make(crypto.randomUUID());
+ const newToolCallId = (): ExecutionToolCallId =>
+ ExecutionToolCallId.make(crypto.randomUUID());
+
+ const ownerScopeId = () => executor.scopes[0]!.id;
+
+ /** Wrap a SandboxToolInvoker so every `invoke` records a
+ * `execution_tool_call` row (running → completed|failed). Storage
+ * failures are swallowed so the tool call itself can never fail
+ * from a bookkeeping error. */
+ const makeRecordingInvoker = (
+ inner: SandboxToolInvoker,
+ executionId: ExecutionId,
+ counter: Ref.Ref,
+ ): SandboxToolInvoker => ({
+ invoke: ({ path, args }) =>
+ Effect.gen(function* () {
+ const callId = newToolCallId();
+ const startedAt = Date.now();
+ yield* executor.executions
+ .recordToolCall({
+ id: callId,
+ executionId,
+ toolPath: path,
+ argsJson: args === undefined ? undefined : safeStringify(args),
+ startedAt,
+ })
+ .pipe(silent);
+ yield* Ref.update(counter, (n) => n + 1);
+
+ return yield* inner.invoke({ path, args }).pipe(
+ Effect.tap((result) =>
+ executor.executions
+ .finishToolCall(callId, {
+ status: "completed",
+ resultJson: result === undefined ? null : safeStringify(result),
+ completedAt: Date.now(),
+ durationMs: Date.now() - startedAt,
+ })
+ .pipe(silent),
+ ),
+ Effect.tapError((err) =>
+ executor.executions
+ .finishToolCall(callId, {
+ status: "failed",
+ errorText: formatErrorMessage(err),
+ completedAt: Date.now(),
+ durationMs: Date.now() - startedAt,
+ })
+ .pipe(silent),
+ ),
+ );
+ }),
+ });
+
+ /** Common post-run update. Runs once per execution on the Exit of
+ * the code-executor fiber — writes final status, result/error,
+ * logs, tool-call count, and completedAt. Ignores storage errors. */
+ const persistTerminalState = (
+ executionId: ExecutionId,
+ exit:
+ | { readonly _tag: "Success"; readonly result: ExecuteResult }
+ | { readonly _tag: "Failure"; readonly cause: Cause.Cause },
+ counter: Ref.Ref,
+ ): Effect.Effect =>
+ Effect.gen(function* () {
+ const toolCallCount = yield* Ref.get(counter);
+ const completedAt = Date.now();
+
+ if (exit._tag === "Success") {
+ const { result } = exit;
+ const hadError = Boolean(result.error);
+ yield* executor.executions
+ .update(executionId, {
+ status: hadError ? "failed" : "completed",
+ resultJson:
+ result.result === undefined ? null : safeStringify(result.result),
+ errorText: result.error ?? null,
+ logsJson:
+ result.logs && result.logs.length > 0
+ ? safeStringify(result.logs)
+ : null,
+ completedAt,
+ toolCallCount,
+ })
+ .pipe(silent);
+ return;
+ }
+
+ yield* executor.executions
+ .update(executionId, {
+ status: "failed",
+ errorText: formatCauseMessage(exit.cause),
+ completedAt,
+ toolCallCount,
+ })
+ .pipe(silent);
+ });
/**
* Race a running fiber against a pause signal. Returns when either
* the fiber completes or an elicitation handler fires (whichever
* comes first). Re-used by both executeWithPause and resume.
+ *
+ * On fiber completion (success or failure) we finalize the
+ * execution row here so persistence happens exactly once per run
+ * regardless of whether the caller pauses first.
*/
const awaitCompletionOrPause = (
fiber: Fiber.Fiber,
pauseSignal: Deferred.Deferred>,
+ executionId: ExecutionId,
+ counter: Ref.Ref,
): Effect.Effect =>
Effect.race(
Fiber.join(fiber).pipe(
+ Effect.tap((result) =>
+ persistTerminalState(executionId, { _tag: "Success", result }, counter),
+ ),
+ Effect.tapErrorCause((cause) =>
+ persistTerminalState(executionId, { _tag: "Failure", cause }, counter),
+ ),
Effect.map((result): ExecutionResult => ({ status: "completed", result })),
),
Deferred.await(pauseSignal).pipe(
@@ -344,12 +528,33 @@ export const createExecutionEngine = <
* The sandbox is forked as a daemon because paused executions can outlive the
* caller scope that returned the first pause, such as an HTTP request handler.
*/
- const startPausableExecution = Effect.fn("mcp.execute")(function* (code: string) {
+ const startPausableExecution = Effect.fn("mcp.execute")(function* (
+ code: string,
+ options?: { readonly trigger?: ExecutionTrigger },
+ ) {
yield* Effect.annotateCurrentSpan({
"mcp.execute.mode": "pausable",
"mcp.execute.code_length": code.length,
});
+ const executionId = newExecutionId();
+ const counter = yield* Ref.make(0);
+ toolCallCounters.set(executionId, counter);
+
+ yield* executor.executions
+ .create({
+ id: executionId,
+ scopeId: ownerScopeId(),
+ status: "running",
+ code,
+ startedAt: Date.now(),
+ triggerKind: options?.trigger?.kind,
+ triggerMetaJson: options?.trigger?.meta
+ ? safeStringify(options.trigger.meta)
+ : undefined,
+ })
+ .pipe(silent);
+
// Ref holds the current pause signal. The elicitation handler reads
// it each time it fires, so resume() can swap in a fresh Deferred
// before unblocking the fiber.
@@ -361,16 +566,31 @@ export const createExecutionEngine = <
const elicitationHandler: ElicitationHandler = (ctx) =>
Effect.gen(function* () {
const responseDeferred = yield* Deferred.make();
- const id = `exec_${++nextId}`;
+ const interactionId = newInteractionId();
+
+ yield* executor.executions
+ .update(executionId, { status: "waiting_for_interaction" })
+ .pipe(silent);
+ yield* executor.executions
+ .recordInteraction({
+ id: interactionId,
+ executionId,
+ status: "pending",
+ kind: ctx.request._tag,
+ purpose: ctx.request.message,
+ payloadJson: safeStringify(serializeElicitationRequest(ctx)),
+ })
+ .pipe(silent);
const paused: InternalPausedExecution = {
- id,
+ id: executionId,
elicitationContext: ctx,
response: responseDeferred,
fiber: fiber!,
pauseSignalRef,
+ interactionId,
};
- pausedExecutions.set(id, paused);
+ pausedExecutions.set(executionId, paused);
const currentSignal = yield* Ref.get(pauseSignalRef);
yield* Deferred.succeed(currentSignal, paused);
@@ -379,13 +599,19 @@ export const createExecutionEngine = <
return yield* Deferred.await(responseDeferred);
});
- const invoker = makeFullInvoker(executor, { onElicitation: elicitationHandler });
+ const fullInvoker = makeFullInvoker(executor, { onElicitation: elicitationHandler });
+ const invoker = makeRecordingInvoker(fullInvoker, executionId, counter);
fiber = yield* Effect.forkDaemon(
codeExecutor.execute(code, invoker).pipe(Effect.withSpan("executor.code.exec")),
);
const initialSignal = yield* Ref.get(pauseSignalRef);
- return (yield* awaitCompletionOrPause(fiber, initialSignal)) as ExecutionResult;
+ return (yield* awaitCompletionOrPause(
+ fiber,
+ initialSignal,
+ executionId,
+ counter,
+ )) as ExecutionResult;
});
/**
@@ -405,6 +631,21 @@ export const createExecutionEngine = <
if (!paused) return null;
pausedExecutions.delete(executionId);
+ const interactionStatus =
+ response.action === "cancel" ? "cancelled" : "resolved";
+ yield* executor.executions
+ .resolveInteraction(paused.interactionId, {
+ status: interactionStatus,
+ responseJson: safeStringify({
+ action: response.action,
+ content: response.content ?? null,
+ }),
+ })
+ .pipe(silent);
+ yield* executor.executions
+ .update(ExecutionId.make(executionId), { status: "running" })
+ .pipe(silent);
+
// Swap in a fresh pause signal BEFORE unblocking the fiber, so the
// next elicitation handler call signals this new Deferred.
const nextSignal = yield* Deferred.make>();
@@ -415,7 +656,14 @@ export const createExecutionEngine = <
content: response.content,
});
- return (yield* awaitCompletionOrPause(paused.fiber, nextSignal)) as ExecutionResult;
+ const counter =
+ toolCallCounters.get(executionId) ?? (yield* Ref.make(0));
+ return (yield* awaitCompletionOrPause(
+ paused.fiber,
+ nextSignal,
+ ExecutionId.make(executionId),
+ counter,
+ )) as ExecutionResult;
});
/**
@@ -424,18 +672,74 @@ export const createExecutionEngine = <
*/
const runInlineExecution = Effect.fn("mcp.execute")(function* (
code: string,
- options: { readonly onElicitation: ElicitationHandler },
+ options: {
+ readonly onElicitation: ElicitationHandler;
+ readonly trigger?: ExecutionTrigger;
+ },
) {
yield* Effect.annotateCurrentSpan({
"mcp.execute.mode": "inline",
"mcp.execute.code_length": code.length,
});
- const invoker = makeFullInvoker(executor, {
- onElicitation: options.onElicitation,
+ const executionId = newExecutionId();
+ const counter = yield* Ref.make(0);
+
+ yield* executor.executions
+ .create({
+ id: executionId,
+ scopeId: ownerScopeId(),
+ status: "running",
+ code,
+ startedAt: Date.now(),
+ triggerKind: options.trigger?.kind,
+ triggerMetaJson: options.trigger?.meta
+ ? safeStringify(options.trigger.meta)
+ : undefined,
+ })
+ .pipe(silent);
+
+ const recordingInteractionHandler: ElicitationHandler = (ctx) =>
+ Effect.gen(function* () {
+ const interactionId = newInteractionId();
+ yield* executor.executions
+ .recordInteraction({
+ id: interactionId,
+ executionId,
+ status: "pending",
+ kind: ctx.request._tag,
+ purpose: ctx.request.message,
+ payloadJson: safeStringify(serializeElicitationRequest(ctx)),
+ })
+ .pipe(silent);
+ const response = yield* options.onElicitation(ctx);
+ yield* executor.executions
+ .resolveInteraction(interactionId, {
+ status: response.action === "cancel" ? "cancelled" : "resolved",
+ responseJson: safeStringify({
+ action: response.action,
+ content: response.content ?? null,
+ }),
+ })
+ .pipe(silent);
+ return response;
+ });
+
+ const fullInvoker = makeFullInvoker(executor, {
+ onElicitation: recordingInteractionHandler,
});
+ const invoker = makeRecordingInvoker(fullInvoker, executionId, counter);
+
return yield* codeExecutor
.execute(code, invoker)
- .pipe(Effect.withSpan("executor.code.exec"));
+ .pipe(
+ Effect.withSpan("executor.code.exec"),
+ Effect.tap((result) =>
+ persistTerminalState(executionId, { _tag: "Success", result }, counter),
+ ),
+ Effect.tapErrorCause((cause) =>
+ persistTerminalState(executionId, { _tag: "Failure", cause }, counter),
+ ),
+ );
});
return {
diff --git a/packages/core/sdk/src/core-schema.ts b/packages/core/sdk/src/core-schema.ts
index 285eec8e5..774c1f8d3 100644
--- a/packages/core/sdk/src/core-schema.ts
+++ b/packages/core/sdk/src/core-schema.ts
@@ -148,6 +148,75 @@ export const coreSchema = {
updated_at: { type: "date", required: true },
},
},
+ // Execution history — one row per `engine.execute()` /
+ // `engine.executeWithPause()`. Captures the submitted code, final
+ // status/result, and trigger metadata. Tool calls and interactions
+ // link back via `execution_id`.
+ execution: {
+ fields: {
+ id: { type: "string", required: true },
+ scope_id: { type: "string", required: true, index: true },
+ status: { type: "string", required: true, index: true },
+ code: { type: "string", required: true },
+ result_json: { type: "string", required: false },
+ error_text: { type: "string", required: false },
+ logs_json: { type: "string", required: false },
+ /** Epoch ms — the point the engine accepted the code. */
+ started_at: { type: "number", required: false },
+ /** Epoch ms — the point the engine reached a terminal status. */
+ completed_at: { type: "number", required: false },
+ /** Free-form trigger kind attributed by the host — `"cli"`,
+ * `"http"`, `"mcp"`, etc. Null when the host didn't attribute
+ * one. Indexed so filter facets scan fast. */
+ trigger_kind: { type: "string", required: false, index: true },
+ /** Opaque host-owned JSON for per-trigger details. */
+ trigger_meta_json: { type: "string", required: false },
+ tool_call_count: { type: "number", required: true, defaultValue: 0 },
+ created_at: { type: "date", required: true, index: true },
+ updated_at: { type: "date", required: true },
+ },
+ },
+ // Per-execution interaction rows — elicitation requests and their
+ // resolutions. A pending row is the hook the runs UI uses to render
+ // the "waiting for input" state; once resolved, `response_json`
+ // captures the user's answer for replay / auditing.
+ execution_interaction: {
+ fields: {
+ id: { type: "string", required: true },
+ execution_id: { type: "string", required: true, index: true },
+ status: { type: "string", required: true, index: true },
+ kind: { type: "string", required: true },
+ purpose: { type: "string", required: false },
+ payload_json: { type: "string", required: false },
+ response_json: { type: "string", required: false },
+ /** Stores sensitive per-response data (e.g. raw form values) that
+ * should not be replayed in the public interaction log. */
+ response_private_json: { type: "string", required: false },
+ created_at: { type: "date", required: true },
+ updated_at: { type: "date", required: true },
+ },
+ },
+ // Per-execution tool-call rows — one per `executor.tools.invoke` that
+ // ran inside the sandboxed execution. Used to build the tool-call
+ // timeline shown in the runs UI.
+ execution_tool_call: {
+ fields: {
+ id: { type: "string", required: true },
+ execution_id: { type: "string", required: true, index: true },
+ status: { type: "string", required: true },
+ /** Dotted tool path (e.g. `github.issues.create`). Indexed so the
+ * facets query in the runs UI resolves without a table scan. */
+ tool_path: { type: "string", required: true, index: true },
+ /** First path segment, pre-computed for cheap faceting. */
+ namespace: { type: "string", required: false, index: true },
+ args_json: { type: "string", required: false },
+ result_json: { type: "string", required: false },
+ error_text: { type: "string", required: false },
+ started_at: { type: "number", required: true },
+ completed_at: { type: "number", required: false },
+ duration_ms: { type: "number", required: false },
+ },
+ },
} as const satisfies DBSchema;
export type CoreSchema = typeof coreSchema;
@@ -176,6 +245,21 @@ export type ConnectionRow = InferDBFieldsOutput<
> &
Record;
+export type ExecutionRow = InferDBFieldsOutput<
+ CoreSchema["execution"]["fields"]
+> &
+ Record;
+
+export type ExecutionInteractionRow = InferDBFieldsOutput<
+ CoreSchema["execution_interaction"]["fields"]
+> &
+ Record;
+
+export type ExecutionToolCallRow = InferDBFieldsOutput<
+ CoreSchema["execution_tool_call"]["fields"]
+> &
+ Record;
+
// ---------------------------------------------------------------------------
// Tool annotations — default-policy metadata the executor consults
// before invocation. Returned by `plugin.resolveAnnotations` (dynamic
diff --git a/packages/core/sdk/src/cursor.ts b/packages/core/sdk/src/cursor.ts
new file mode 100644
index 000000000..ffa367ee5
--- /dev/null
+++ b/packages/core/sdk/src/cursor.ts
@@ -0,0 +1,44 @@
+// ---------------------------------------------------------------------------
+// Opaque cursor helpers for ExecutionStore.list pagination.
+//
+// Cursors encode `{ createdAt, id }` — the tuple adapter backends need to
+// resume a scan on `ORDER BY created_at DESC, id DESC`. Encoded as
+// url-safe base64 so callers can pass them through query params without
+// thinking about escaping.
+// ---------------------------------------------------------------------------
+
+export interface CursorPayload {
+ readonly createdAt: number;
+ readonly id: string;
+}
+
+const toBase64Url = (value: string): string => {
+ const bytes = new TextEncoder().encode(value);
+ let binary = "";
+ for (const b of bytes) binary += String.fromCharCode(b);
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
+};
+
+const fromBase64Url = (value: string): string => {
+ const pad = value.length % 4 === 0 ? 0 : 4 - (value.length % 4);
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(pad);
+ const binary = atob(normalized);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
+ return new TextDecoder().decode(bytes);
+};
+
+export const encodeCursor = (payload: CursorPayload): string =>
+ toBase64Url(JSON.stringify(payload));
+
+export const decodeCursor = (raw: string): CursorPayload | null => {
+ try {
+ const parsed = JSON.parse(fromBase64Url(raw)) as Record;
+ if (typeof parsed.createdAt !== "number" || typeof parsed.id !== "string") {
+ return null;
+ }
+ return { createdAt: parsed.createdAt, id: parsed.id };
+ } catch {
+ return null;
+ }
+};
diff --git a/packages/core/sdk/src/execution-store.ts b/packages/core/sdk/src/execution-store.ts
new file mode 100644
index 000000000..f2725ba98
--- /dev/null
+++ b/packages/core/sdk/src/execution-store.ts
@@ -0,0 +1,556 @@
+// ---------------------------------------------------------------------------
+// makeExecutionStore — an ExecutionStoreService implementation backed by
+// the generic `typedAdapter` surface. Used by createExecutor
+// to expose `executor.executions`.
+//
+// Per-row JSON columns (`result_json`, `logs_json`, `payload_json`, …)
+// are stored as opaque strings — the SDK does not inspect their shape.
+// Callers pre-stringify when writing and parse when reading.
+// ---------------------------------------------------------------------------
+
+import { Effect } from "effect";
+import type { StorageFailure, TypedAdapter } from "@executor/storage-core";
+
+import type { CoreSchema } from "./core-schema";
+
+// Row shape accepted by the row-to-class mappers. We can't rely on
+// `RowOutput` narrowing because `TypedAdapter.update`
+// takes a `Partial>` payload — Partial strips every
+// required field, so TypeScript falls back to a union of every row
+// type in the schema. The mappers read fields by name with explicit
+// `row.xxx as string` casts anyway, so typing the input as the base
+// record that every `RowOutput` extends is just as safe.
+type AdapterRow = Record;
+import { decodeCursor, encodeCursor } from "./cursor";
+import {
+ Execution,
+ ExecutionInteraction,
+ ExecutionToolCall,
+ type ExecutionStoreService,
+ type ExecutionListOptions,
+ type ExecutionListResult,
+ type ExecutionListItem,
+ type ExecutionListMeta,
+ type ExecutionChartBucket,
+ type ExecutionStatus,
+ type ExecutionStatusCount,
+ type ExecutionTriggerCount,
+ type ExecutionToolFacet,
+ EXECUTION_STATUS_KEYS,
+} from "./executions";
+import {
+ ExecutionId,
+ ExecutionInteractionId,
+ ExecutionToolCallId,
+ ScopeId,
+} from "./ids";
+
+const DEFAULT_LIMIT = 25;
+const MAX_LIMIT = 100;
+
+const toNumberOrNull = (value: unknown): number | null =>
+ value == null ? null : Number(value);
+
+const toStringOrNull = (value: unknown): string | null =>
+ value == null ? null : (value as string);
+
+const toDate = (value: unknown): Date =>
+ value instanceof Date ? value : new Date(value as string | number);
+
+const rowToExecution = (row: AdapterRow): Execution =>
+ new Execution({
+ id: ExecutionId.make(row.id as string),
+ scopeId: ScopeId.make(row.scope_id as string),
+ status: row.status as ExecutionStatus,
+ code: row.code as string,
+ resultJson: toStringOrNull(row.result_json),
+ errorText: toStringOrNull(row.error_text),
+ logsJson: toStringOrNull(row.logs_json),
+ startedAt: toNumberOrNull(row.started_at),
+ completedAt: toNumberOrNull(row.completed_at),
+ triggerKind: toStringOrNull(row.trigger_kind),
+ triggerMetaJson: toStringOrNull(row.trigger_meta_json),
+ toolCallCount: Number(row.tool_call_count ?? 0),
+ createdAt: toDate(row.created_at),
+ updatedAt: toDate(row.updated_at),
+ });
+
+const rowToInteraction = (row: AdapterRow): ExecutionInteraction =>
+ new ExecutionInteraction({
+ id: ExecutionInteractionId.make(row.id as string),
+ executionId: ExecutionId.make(row.execution_id as string),
+ status: row.status as ExecutionInteraction["status"],
+ kind: row.kind as string,
+ purpose: toStringOrNull(row.purpose),
+ payloadJson: toStringOrNull(row.payload_json),
+ responseJson: toStringOrNull(row.response_json),
+ responsePrivateJson: toStringOrNull(row.response_private_json),
+ createdAt: toDate(row.created_at),
+ updatedAt: toDate(row.updated_at),
+ });
+
+const rowToToolCall = (row: AdapterRow): ExecutionToolCall =>
+ new ExecutionToolCall({
+ id: ExecutionToolCallId.make(row.id as string),
+ executionId: ExecutionId.make(row.execution_id as string),
+ status: row.status as ExecutionToolCall["status"],
+ toolPath: row.tool_path as string,
+ namespace: toStringOrNull(row.namespace),
+ argsJson: toStringOrNull(row.args_json),
+ resultJson: toStringOrNull(row.result_json),
+ errorText: toStringOrNull(row.error_text),
+ startedAt: Number(row.started_at),
+ completedAt: toNumberOrNull(row.completed_at),
+ durationMs: toNumberOrNull(row.duration_ms),
+ });
+
+const pickChartBucketMs = (windowMs: number): number => {
+ if (windowMs <= 15 * 60_000) return 30_000; // <=15m → 30s
+ if (windowMs <= 60 * 60_000) return 120_000; // <=1h → 2m
+ if (windowMs <= 24 * 60 * 60_000) return 30 * 60_000; // <=24h → 30m
+ if (windowMs <= 7 * 24 * 60 * 60_000) return 3 * 60 * 60_000; // <=7d → 3h
+ return 24 * 60 * 60_000; // else → 1d
+};
+
+const matchesToolGlob = (toolPath: string, pattern: string): boolean => {
+ if (pattern === toolPath) return true;
+ if (pattern.endsWith(".*")) {
+ const prefix = pattern.slice(0, -2);
+ return toolPath === prefix || toolPath.startsWith(`${prefix}.`);
+ }
+ return false;
+};
+
+export interface MakeExecutionStoreOptions {
+ readonly core: TypedAdapter;
+ readonly now?: () => Date;
+}
+
+export const makeExecutionStore = ({
+ core,
+ now = () => new Date(),
+}: MakeExecutionStoreOptions): ExecutionStoreService => {
+ const create: ExecutionStoreService["create"] = (input) =>
+ Effect.gen(function* () {
+ const timestamp = now();
+ const row = yield* core.create({
+ model: "execution",
+ forceAllowId: true,
+ data: {
+ id: input.id,
+ scope_id: input.scopeId,
+ status: input.status,
+ code: input.code,
+ result_json: null,
+ error_text: null,
+ logs_json: null,
+ started_at: input.startedAt ?? timestamp.getTime(),
+ completed_at: null,
+ trigger_kind: input.triggerKind ?? null,
+ trigger_meta_json: input.triggerMetaJson ?? null,
+ tool_call_count: 0,
+ created_at: timestamp,
+ updated_at: timestamp,
+ },
+ });
+ return rowToExecution(row);
+ });
+
+ const update: ExecutionStoreService["update"] = (id, patch) =>
+ Effect.gen(function* () {
+ const row = yield* core.update({
+ model: "execution",
+ where: [{ field: "id", value: id as string }],
+ update: {
+ updated_at: now(),
+ ...(patch.status !== undefined && { status: patch.status }),
+ ...(patch.resultJson !== undefined && { result_json: patch.resultJson }),
+ ...(patch.errorText !== undefined && { error_text: patch.errorText }),
+ ...(patch.logsJson !== undefined && { logs_json: patch.logsJson }),
+ ...(patch.completedAt !== undefined && { completed_at: patch.completedAt }),
+ ...(patch.toolCallCount !== undefined && {
+ tool_call_count: patch.toolCallCount,
+ }),
+ },
+ });
+ if (!row) return yield* Effect.die(`Execution ${id} vanished during update`);
+ return rowToExecution(row);
+ });
+
+ const get: ExecutionStoreService["get"] = (id) =>
+ Effect.gen(function* () {
+ const rows = yield* core.findMany({
+ model: "execution",
+ where: [{ field: "id", value: id as string }],
+ limit: 1,
+ });
+ const execution = rows[0];
+ if (!execution) return null;
+ const interactions = yield* core.findMany({
+ model: "execution_interaction",
+ where: [
+ { field: "execution_id", value: id as string },
+ { field: "status", value: "pending" },
+ ],
+ sortBy: { field: "created_at", direction: "desc" },
+ limit: 1,
+ });
+ const pendingInteraction = interactions[0]
+ ? rowToInteraction(interactions[0])
+ : null;
+ return {
+ execution: rowToExecution(execution),
+ pendingInteraction,
+ };
+ });
+
+ const recordInteraction: ExecutionStoreService["recordInteraction"] = (input) =>
+ Effect.gen(function* () {
+ const timestamp = now();
+ const row = yield* core.create({
+ model: "execution_interaction",
+ forceAllowId: true,
+ data: {
+ id: input.id,
+ execution_id: input.executionId,
+ status: input.status,
+ kind: input.kind,
+ purpose: input.purpose ?? null,
+ payload_json: input.payloadJson ?? null,
+ response_json: null,
+ response_private_json: null,
+ created_at: timestamp,
+ updated_at: timestamp,
+ },
+ });
+ return rowToInteraction(row);
+ });
+
+ const resolveInteraction: ExecutionStoreService["resolveInteraction"] = (id, patch) =>
+ Effect.gen(function* () {
+ const row = yield* core.update({
+ model: "execution_interaction",
+ where: [{ field: "id", value: id as string }],
+ update: {
+ updated_at: now(),
+ ...(patch.status !== undefined && { status: patch.status }),
+ ...(patch.responseJson !== undefined && { response_json: patch.responseJson }),
+ ...(patch.responsePrivateJson !== undefined && {
+ response_private_json: patch.responsePrivateJson,
+ }),
+ },
+ });
+ if (!row)
+ return yield* Effect.die(`Interaction ${id} vanished during update`);
+ return rowToInteraction(row);
+ });
+
+ const recordToolCall: ExecutionStoreService["recordToolCall"] = (input) =>
+ Effect.gen(function* () {
+ const row = yield* core.create({
+ model: "execution_tool_call",
+ forceAllowId: true,
+ data: {
+ id: input.id,
+ execution_id: input.executionId,
+ status: "running",
+ tool_path: input.toolPath,
+ namespace: input.namespace ?? input.toolPath.split(".")[0] ?? null,
+ args_json: input.argsJson ?? null,
+ result_json: null,
+ error_text: null,
+ started_at: input.startedAt,
+ completed_at: null,
+ duration_ms: null,
+ },
+ });
+ return rowToToolCall(row);
+ });
+
+ const finishToolCall: ExecutionStoreService["finishToolCall"] = (id, patch) =>
+ Effect.gen(function* () {
+ const row = yield* core.update({
+ model: "execution_tool_call",
+ where: [{ field: "id", value: id as string }],
+ update: {
+ status: patch.status,
+ result_json: patch.resultJson ?? null,
+ error_text: patch.errorText ?? null,
+ completed_at: patch.completedAt,
+ duration_ms: patch.durationMs,
+ },
+ });
+ if (!row) return yield* Effect.die(`Tool call ${id} vanished during finish`);
+ return rowToToolCall(row);
+ });
+
+ const listToolCalls: ExecutionStoreService["listToolCalls"] = (executionId) =>
+ Effect.gen(function* () {
+ const rows = yield* core.findMany({
+ model: "execution_tool_call",
+ where: [{ field: "execution_id", value: executionId as string }],
+ sortBy: { field: "started_at", direction: "asc" },
+ });
+ return rows.map(rowToToolCall);
+ });
+
+ const sweep: ExecutionStoreService["sweep"] = (olderThanMs) =>
+ Effect.gen(function* () {
+ const cutoff = new Date(now().getTime() - olderThanMs);
+ // Adapter deleteMany returns void in the generic contract — we do
+ // a pre-count scan so the caller gets a useful number back.
+ const doomed = yield* core.findMany({
+ model: "execution",
+ where: [{ field: "created_at", operator: "lt", value: cutoff }],
+ limit: 10_000,
+ });
+ if (doomed.length === 0) return 0;
+ yield* core.deleteMany({
+ model: "execution",
+ where: [{ field: "created_at", operator: "lt", value: cutoff }],
+ });
+ return doomed.length;
+ });
+
+ const list: ExecutionStoreService["list"] = (scopeId, rawOptions) =>
+ Effect.gen(function* () {
+ const options: ExecutionListOptions = rawOptions ?? {};
+ const limit = Math.max(1, Math.min(options.limit ?? DEFAULT_LIMIT, MAX_LIMIT));
+ const sort = options.sort ?? { field: "createdAt", direction: "desc" as const };
+
+ // Pull candidate rows for this scope. We apply filters in-memory —
+ // the DBAdapter contract's Where clauses don't cover compound
+ // CSV-of-values or glob patterns, so the store does the final
+ // narrowing after fetching.
+ const rows = yield* core.findMany({
+ model: "execution",
+ where: [{ field: "scope_id", value: scopeId as string }],
+ sortBy: {
+ field: sort.field === "durationMs" ? "completed_at" : "created_at",
+ direction: sort.direction,
+ },
+ });
+
+ // Pre-compute tool-call aggregations that filters and meta both need.
+ const toolCallRows = yield* core.findMany({
+ model: "execution_tool_call",
+ });
+
+ const toolCallsByExecution = new Map();
+ for (const tc of toolCallRows) {
+ const list = toolCallsByExecution.get(tc.execution_id as string) ?? [];
+ list.push(tc);
+ toolCallsByExecution.set(tc.execution_id as string, list);
+ }
+
+ // Pre-compute interaction presence per execution (for the
+ // hadElicitation filter + meta interactionCounts).
+ const interactionRows = yield* core.findMany({
+ model: "execution_interaction",
+ });
+ const executionsWithInteractions = new Set(
+ interactionRows.map((r) => r.execution_id as string),
+ );
+
+ const appliesStatusFilter = (row: AdapterRow): boolean =>
+ !options.statusFilter || options.statusFilter.length === 0
+ ? true
+ : options.statusFilter.includes(row.status as ExecutionStatus);
+
+ const appliesTriggerFilter = (row: AdapterRow): boolean => {
+ if (!options.triggerFilter || options.triggerFilter.length === 0) return true;
+ const kind = (row.trigger_kind as string | null | undefined) ?? null;
+ return options.triggerFilter.some((want) =>
+ want === "unknown" ? kind === null : want === kind,
+ );
+ };
+
+ const appliesToolFilter = (row: AdapterRow): boolean => {
+ if (!options.toolPathFilter || options.toolPathFilter.length === 0) return true;
+ const calls = toolCallsByExecution.get(row.id as string) ?? [];
+ return options.toolPathFilter.some((pattern) =>
+ calls.some((c) => matchesToolGlob(c.tool_path as string, pattern)),
+ );
+ };
+
+ const appliesTimeFilter = (row: AdapterRow): boolean => {
+ const createdAt = toDate(row.created_at).getTime();
+ if (options.timeRange?.from !== undefined && createdAt < options.timeRange.from) {
+ return false;
+ }
+ if (options.timeRange?.to !== undefined && createdAt > options.timeRange.to) {
+ return false;
+ }
+ if (options.after !== undefined) {
+ const afterMs = Number(options.after);
+ if (!Number.isNaN(afterMs) && createdAt <= afterMs) return false;
+ }
+ return true;
+ };
+
+ const appliesCodeQuery = (row: AdapterRow): boolean =>
+ !options.codeQuery
+ ? true
+ : (row.code as string).toLowerCase().includes(options.codeQuery.toLowerCase());
+
+ const appliesElicitationFilter = (row: AdapterRow): boolean => {
+ if (options.hadElicitation === undefined) return true;
+ const has = executionsWithInteractions.has(row.id as string);
+ return options.hadElicitation ? has : !has;
+ };
+
+ const filtered = rows.filter(
+ (row) =>
+ appliesStatusFilter(row) &&
+ appliesTriggerFilter(row) &&
+ appliesToolFilter(row) &&
+ appliesTimeFilter(row) &&
+ appliesCodeQuery(row) &&
+ appliesElicitationFilter(row),
+ );
+
+ // Cursor applies after filtering so it tracks the filtered scan.
+ const cursor = options.cursor ? decodeCursor(options.cursor) : null;
+ const afterCursor = cursor
+ ? filtered.filter((row) => {
+ const createdAt = toDate(row.created_at).getTime();
+ if (createdAt < cursor.createdAt) return true;
+ if (createdAt > cursor.createdAt) return false;
+ return (row.id as string) < cursor.id;
+ })
+ : filtered;
+
+ const page = afterCursor.slice(0, limit);
+ const nextCursor =
+ afterCursor.length > limit && sort.field === "createdAt"
+ ? encodeCursor({
+ createdAt: toDate(page[page.length - 1]!.created_at).getTime(),
+ id: page[page.length - 1]!.id as string,
+ })
+ : undefined;
+
+ const pageIds = page.map((r) => r.id as string);
+ const pendingByExecution = new Map();
+ for (const interaction of interactionRows) {
+ if (interaction.status !== "pending") continue;
+ if (!pageIds.includes(interaction.execution_id as string)) continue;
+ const existing = pendingByExecution.get(interaction.execution_id as string);
+ if (!existing || toDate(interaction.created_at).getTime() >
+ toDate(existing.created_at).getTime()) {
+ pendingByExecution.set(interaction.execution_id as string, interaction);
+ }
+ }
+
+ const executions: readonly ExecutionListItem[] = page.map((row) => ({
+ execution: rowToExecution(row),
+ pendingInteraction: pendingByExecution.has(row.id as string)
+ ? rowToInteraction(pendingByExecution.get(row.id as string)!)
+ : null,
+ }));
+
+ const meta: ExecutionListMeta | undefined = options.includeMeta
+ ? buildMeta(rows, filtered, toolCallsByExecution, executionsWithInteractions)
+ : undefined;
+
+ return {
+ executions,
+ ...(nextCursor ? { nextCursor } : {}),
+ ...(meta ? { meta } : {}),
+ } satisfies ExecutionListResult;
+ });
+
+ const buildMeta = (
+ all: readonly AdapterRow[],
+ filtered: readonly AdapterRow[],
+ toolCallsByExecution: Map,
+ executionsWithInteractions: Set,
+ ): ExecutionListMeta => {
+ const statusCounts: ExecutionStatusCount[] = EXECUTION_STATUS_KEYS.map((status) => ({
+ status,
+ count: filtered.filter((r) => r.status === status).length,
+ }));
+
+ const triggerMap = new Map();
+ for (const row of filtered) {
+ const kind = (row.trigger_kind as string | null | undefined) ?? null;
+ triggerMap.set(kind, (triggerMap.get(kind) ?? 0) + 1);
+ }
+ const triggerCounts: ExecutionTriggerCount[] = Array.from(triggerMap.entries()).map(
+ ([triggerKind, count]) => ({ triggerKind, count }),
+ );
+
+ const toolCountMap = new Map();
+ for (const row of filtered) {
+ for (const tc of toolCallsByExecution.get(row.id as string) ?? []) {
+ const path = tc.tool_path as string;
+ toolCountMap.set(path, (toolCountMap.get(path) ?? 0) + 1);
+ }
+ }
+ const toolFacets: ExecutionToolFacet[] = Array.from(toolCountMap.entries())
+ .map(([toolPath, count]) => ({ toolPath, count }))
+ .sort((a, b) => b.count - a.count)
+ .slice(0, 20);
+
+ const withElicitation = filtered.filter((r) =>
+ executionsWithInteractions.has(r.id as string),
+ ).length;
+
+ const times = filtered.map((r) => toDate(r.created_at).getTime());
+ const minTs = times.length > 0 ? Math.min(...times) : now().getTime();
+ const maxTs = times.length > 0 ? Math.max(...times) : now().getTime();
+ const windowMs = Math.max(1, maxTs - minTs);
+ const chartBucketMs = pickChartBucketMs(windowMs);
+
+ const bucketMap = new Map>();
+ for (const row of filtered) {
+ const bucketStart =
+ Math.floor(toDate(row.created_at).getTime() / chartBucketMs) * chartBucketMs;
+ const counts = bucketMap.get(bucketStart) ?? {
+ pending: 0,
+ running: 0,
+ waiting_for_interaction: 0,
+ completed: 0,
+ failed: 0,
+ cancelled: 0,
+ };
+ counts[row.status as ExecutionStatus] += 1;
+ bucketMap.set(bucketStart, counts);
+ }
+ const chartData: ExecutionChartBucket[] = Array.from(bucketMap.entries())
+ .map(([bucketStart, counts]) => ({ bucketStart, counts }))
+ .sort((a, b) => a.bucketStart - b.bucketStart);
+
+ return {
+ totalRowCount: all.length,
+ filterRowCount: filtered.length,
+ statusCounts,
+ triggerCounts,
+ toolFacets,
+ interactionCounts: {
+ withElicitation,
+ withoutElicitation: filtered.length - withElicitation,
+ },
+ chartBucketMs,
+ chartData,
+ };
+ };
+
+ return {
+ create,
+ update,
+ get,
+ list,
+ recordInteraction,
+ resolveInteraction,
+ recordToolCall,
+ finishToolCall,
+ listToolCalls,
+ sweep,
+ } satisfies ExecutionStoreService;
+};
+
+// Re-export the Tag symbol here so callers can `import { ExecutionStore }
+// from "@executor/sdk"` and get both the Tag and a layer factory from
+// one module entry.
+export { ExecutionStore } from "./executions";
+export type { StorageFailure };
diff --git a/packages/core/sdk/src/executions.test.ts b/packages/core/sdk/src/executions.test.ts
new file mode 100644
index 000000000..06a289da4
--- /dev/null
+++ b/packages/core/sdk/src/executions.test.ts
@@ -0,0 +1,260 @@
+import { describe, expect, it } from "@effect/vitest";
+import { Effect } from "effect";
+
+import { makeMemoryAdapter } from "@executor/storage-core/testing/memory";
+
+import { collectSchemas, createExecutor } from "./executor";
+import {
+ EXECUTION_STATUS_KEYS,
+ type ExecutionStatus,
+} from "./executions";
+import {
+ ExecutionId,
+ ExecutionInteractionId,
+ ExecutionToolCallId,
+ ScopeId,
+} from "./ids";
+import { Scope } from "./scope";
+import { makeInMemoryBlobStore } from "./blob";
+
+// ---------------------------------------------------------------------------
+// Shared fixture. Every test builds a scoped executor backed by the
+// in-memory adapter — zero persistence, zero migration dance.
+// ---------------------------------------------------------------------------
+
+const SCOPE = ScopeId.make("scope-test");
+
+const makeExecutor = () =>
+ Effect.gen(function* () {
+ const schema = collectSchemas([]);
+ const adapter = makeMemoryAdapter({ schema });
+ const scope = new Scope({ id: SCOPE, name: "test", createdAt: new Date() });
+ const executor = yield* createExecutor({
+ scopes: [scope],
+ adapter,
+ blobs: makeInMemoryBlobStore(),
+ });
+ return executor;
+ });
+
+describe("ExecutionStore (DBAdapter-backed)", () => {
+ it.effect("create → get round-trip preserves fields", () =>
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor();
+ const id = ExecutionId.make("exec-1");
+
+ yield* executor.executions.create({
+ id,
+ scopeId: SCOPE,
+ status: "running",
+ code: "const x = 1",
+ triggerKind: "cli",
+ });
+
+ const detail = yield* executor.executions.get(id);
+ expect(detail).not.toBeNull();
+ expect(detail!.execution.id).toBe(id);
+ expect(detail!.execution.status).toBe("running");
+ expect(detail!.execution.code).toBe("const x = 1");
+ expect(detail!.execution.triggerKind).toBe("cli");
+ expect(detail!.execution.toolCallCount).toBe(0);
+ expect(detail!.pendingInteraction).toBeNull();
+ }),
+ );
+
+ it.effect("update patches status, result, and completion time", () =>
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor();
+ const id = ExecutionId.make("exec-2");
+
+ yield* executor.executions.create({
+ id,
+ scopeId: SCOPE,
+ status: "running",
+ code: "2 + 2",
+ });
+
+ yield* executor.executions.update(id, {
+ status: "completed",
+ resultJson: JSON.stringify({ value: 4 }),
+ completedAt: 1_700_000_000_000,
+ toolCallCount: 1,
+ });
+
+ const detail = yield* executor.executions.get(id);
+ expect(detail!.execution.status).toBe("completed");
+ expect(detail!.execution.resultJson).toBe('{"value":4}');
+ expect(detail!.execution.completedAt).toBe(1_700_000_000_000);
+ expect(detail!.execution.toolCallCount).toBe(1);
+ }),
+ );
+
+ it.effect("tool-call recording + finish updates status + duration", () =>
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor();
+ const executionId = ExecutionId.make("exec-3");
+ const toolCallId = ExecutionToolCallId.make("tc-1");
+
+ yield* executor.executions.create({
+ id: executionId,
+ scopeId: SCOPE,
+ status: "running",
+ code: "await tools.a()",
+ });
+
+ yield* executor.executions.recordToolCall({
+ id: toolCallId,
+ executionId,
+ toolPath: "ns.doThing",
+ startedAt: 1_700_000_000_000,
+ });
+
+ yield* executor.executions.finishToolCall(toolCallId, {
+ status: "completed",
+ resultJson: '{"ok":true}',
+ completedAt: 1_700_000_000_250,
+ durationMs: 250,
+ });
+
+ const calls = yield* executor.executions.listToolCalls(executionId);
+ expect(calls).toHaveLength(1);
+ expect(calls[0]!.status).toBe("completed");
+ expect(calls[0]!.toolPath).toBe("ns.doThing");
+ expect(calls[0]!.namespace).toBe("ns");
+ expect(calls[0]!.durationMs).toBe(250);
+ expect(calls[0]!.resultJson).toBe('{"ok":true}');
+ }),
+ );
+
+ it.effect("interaction lifecycle: record pending → resolve", () =>
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor();
+ const executionId = ExecutionId.make("exec-4");
+ const interactionId = ExecutionInteractionId.make("int-1");
+
+ yield* executor.executions.create({
+ id: executionId,
+ scopeId: SCOPE,
+ status: "waiting_for_interaction",
+ code: "await elicit(...)",
+ });
+
+ yield* executor.executions.recordInteraction({
+ id: interactionId,
+ executionId,
+ status: "pending",
+ kind: "FormElicitation",
+ payloadJson: '{"message":"ok?"}',
+ });
+
+ // get() should surface the pending interaction alongside the row.
+ const beforeResolve = yield* executor.executions.get(executionId);
+ expect(beforeResolve!.pendingInteraction).not.toBeNull();
+ expect(beforeResolve!.pendingInteraction!.id).toBe(interactionId);
+
+ yield* executor.executions.resolveInteraction(interactionId, {
+ status: "resolved",
+ responseJson: '{"action":"accept"}',
+ });
+
+ const afterResolve = yield* executor.executions.get(executionId);
+ expect(afterResolve!.pendingInteraction).toBeNull();
+ }),
+ );
+
+ it.effect("list applies status + trigger filters", () =>
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor();
+
+ yield* executor.executions.create({
+ id: ExecutionId.make("e-a"),
+ scopeId: SCOPE,
+ status: "completed",
+ code: "a",
+ triggerKind: "cli",
+ });
+ yield* executor.executions.create({
+ id: ExecutionId.make("e-b"),
+ scopeId: SCOPE,
+ status: "failed",
+ code: "b",
+ triggerKind: "http",
+ });
+ yield* executor.executions.create({
+ id: ExecutionId.make("e-c"),
+ scopeId: SCOPE,
+ status: "completed",
+ code: "c",
+ triggerKind: "mcp",
+ });
+
+ const completedOnly = yield* executor.executions.list(SCOPE, {
+ statusFilter: ["completed"],
+ });
+ expect(completedOnly.executions).toHaveLength(2);
+
+ const httpOnly = yield* executor.executions.list(SCOPE, {
+ triggerFilter: ["http"],
+ });
+ expect(httpOnly.executions).toHaveLength(1);
+ expect(httpOnly.executions[0]!.execution.id).toBe(ExecutionId.make("e-b"));
+ }),
+ );
+
+ it.effect("list meta reports status + trigger counts", () =>
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor();
+ yield* executor.executions.create({
+ id: ExecutionId.make("m-1"),
+ scopeId: SCOPE,
+ status: "completed",
+ code: "x",
+ triggerKind: "cli",
+ });
+ yield* executor.executions.create({
+ id: ExecutionId.make("m-2"),
+ scopeId: SCOPE,
+ status: "completed",
+ code: "y",
+ triggerKind: "cli",
+ });
+ yield* executor.executions.create({
+ id: ExecutionId.make("m-3"),
+ scopeId: SCOPE,
+ status: "failed",
+ code: "z",
+ triggerKind: "mcp",
+ });
+
+ const res = yield* executor.executions.list(SCOPE, { includeMeta: true });
+ expect(res.meta).toBeDefined();
+ expect(res.meta!.totalRowCount).toBe(3);
+
+ const completedCount = res.meta!.statusCounts.find(
+ (c) => c.status === ("completed" as ExecutionStatus),
+ );
+ expect(completedCount!.count).toBe(2);
+ expect(res.meta!.triggerCounts.find((t) => t.triggerKind === "cli")!.count).toBe(
+ 2,
+ );
+ expect(res.meta!.triggerCounts.find((t) => t.triggerKind === "mcp")!.count).toBe(
+ 1,
+ );
+ }),
+ );
+
+ it.effect("EXECUTION_STATUS_KEYS covers every status literal", () =>
+ Effect.sync(() => {
+ expect(new Set(EXECUTION_STATUS_KEYS)).toEqual(
+ new Set([
+ "pending",
+ "running",
+ "waiting_for_interaction",
+ "completed",
+ "failed",
+ "cancelled",
+ ]),
+ );
+ }),
+ );
+});
diff --git a/packages/core/sdk/src/executions.ts b/packages/core/sdk/src/executions.ts
new file mode 100644
index 000000000..d1b6fee52
--- /dev/null
+++ b/packages/core/sdk/src/executions.ts
@@ -0,0 +1,292 @@
+// ---------------------------------------------------------------------------
+// ExecutionStore — records one run per `engine.execute()` /
+// `executeWithPause()`. Wraps the generic `DBAdapter` core tables
+// (`execution`, `execution_interaction`, `execution_tool_call`) so
+// every storage backend that implements the adapter contract gets
+// execution history for free.
+//
+// The store itself is plain Effect code; the adapter is threaded in
+// by `createExecutor` and exposed to callers as `executor.executions`.
+// ---------------------------------------------------------------------------
+
+import { Context, Effect, Schema } from "effect";
+import type { StorageFailure } from "@executor/storage-core";
+
+import { ExecutionId, ExecutionInteractionId, ExecutionToolCallId, ScopeId } from "./ids";
+
+// ---------------------------------------------------------------------------
+// Status enums
+// ---------------------------------------------------------------------------
+
+export const ExecutionStatus = Schema.Literal(
+ "pending",
+ "running",
+ "waiting_for_interaction",
+ "completed",
+ "failed",
+ "cancelled",
+);
+export type ExecutionStatus = typeof ExecutionStatus.Type;
+
+export const EXECUTION_STATUS_KEYS = [
+ "pending",
+ "running",
+ "waiting_for_interaction",
+ "completed",
+ "failed",
+ "cancelled",
+] as const;
+
+export const ExecutionInteractionStatus = Schema.Literal(
+ "pending",
+ "resolved",
+ "cancelled",
+);
+export type ExecutionInteractionStatus = typeof ExecutionInteractionStatus.Type;
+
+export const ExecutionToolCallStatus = Schema.Literal(
+ "running",
+ "completed",
+ "failed",
+);
+export type ExecutionToolCallStatus = typeof ExecutionToolCallStatus.Type;
+
+// ---------------------------------------------------------------------------
+// Row projections
+// ---------------------------------------------------------------------------
+
+export class Execution extends Schema.Class("Execution")({
+ id: ExecutionId,
+ scopeId: ScopeId,
+ status: ExecutionStatus,
+ code: Schema.String,
+ resultJson: Schema.NullOr(Schema.String),
+ errorText: Schema.NullOr(Schema.String),
+ logsJson: Schema.NullOr(Schema.String),
+ startedAt: Schema.NullOr(Schema.Number),
+ completedAt: Schema.NullOr(Schema.Number),
+ triggerKind: Schema.NullOr(Schema.String),
+ triggerMetaJson: Schema.NullOr(Schema.String),
+ toolCallCount: Schema.Number,
+ createdAt: Schema.DateFromNumber,
+ updatedAt: Schema.DateFromNumber,
+}) {}
+
+export class ExecutionInteraction extends Schema.Class(
+ "ExecutionInteraction",
+)({
+ id: ExecutionInteractionId,
+ executionId: ExecutionId,
+ status: ExecutionInteractionStatus,
+ kind: Schema.String,
+ purpose: Schema.NullOr(Schema.String),
+ payloadJson: Schema.NullOr(Schema.String),
+ responseJson: Schema.NullOr(Schema.String),
+ responsePrivateJson: Schema.NullOr(Schema.String),
+ createdAt: Schema.DateFromNumber,
+ updatedAt: Schema.DateFromNumber,
+}) {}
+
+export class ExecutionToolCall extends Schema.Class("ExecutionToolCall")({
+ id: ExecutionToolCallId,
+ executionId: ExecutionId,
+ status: ExecutionToolCallStatus,
+ toolPath: Schema.String,
+ namespace: Schema.NullOr(Schema.String),
+ argsJson: Schema.NullOr(Schema.String),
+ resultJson: Schema.NullOr(Schema.String),
+ errorText: Schema.NullOr(Schema.String),
+ startedAt: Schema.Number,
+ completedAt: Schema.NullOr(Schema.Number),
+ durationMs: Schema.NullOr(Schema.Number),
+}) {}
+
+// ---------------------------------------------------------------------------
+// Input types
+// ---------------------------------------------------------------------------
+
+export interface CreateExecutionInput {
+ readonly id: ExecutionId;
+ readonly scopeId: ScopeId;
+ readonly status: ExecutionStatus;
+ readonly code: string;
+ readonly startedAt?: number;
+ readonly triggerKind?: string;
+ readonly triggerMetaJson?: string;
+}
+
+export interface UpdateExecutionInput {
+ readonly status?: ExecutionStatus;
+ readonly resultJson?: string | null;
+ readonly errorText?: string | null;
+ readonly logsJson?: string | null;
+ readonly completedAt?: number;
+ readonly toolCallCount?: number;
+}
+
+export interface CreateExecutionInteractionInput {
+ readonly id: ExecutionInteractionId;
+ readonly executionId: ExecutionId;
+ readonly status: ExecutionInteractionStatus;
+ readonly kind: string;
+ readonly purpose?: string;
+ readonly payloadJson?: string;
+}
+
+export interface UpdateExecutionInteractionInput {
+ readonly status?: ExecutionInteractionStatus;
+ readonly responseJson?: string | null;
+ readonly responsePrivateJson?: string | null;
+}
+
+export interface CreateExecutionToolCallInput {
+ readonly id: ExecutionToolCallId;
+ readonly executionId: ExecutionId;
+ readonly toolPath: string;
+ readonly namespace?: string;
+ readonly argsJson?: string;
+ readonly startedAt: number;
+}
+
+export interface UpdateExecutionToolCallInput {
+ readonly status: ExecutionToolCallStatus;
+ readonly resultJson?: string | null;
+ readonly errorText?: string | null;
+ readonly completedAt: number;
+ readonly durationMs: number;
+}
+
+// ---------------------------------------------------------------------------
+// Filters + sort
+// ---------------------------------------------------------------------------
+
+export type ExecutionSortField = "createdAt" | "durationMs";
+export type ExecutionSortDirection = "asc" | "desc";
+export interface ExecutionSort {
+ readonly field: ExecutionSortField;
+ readonly direction: ExecutionSortDirection;
+}
+
+export interface ExecutionTimeRange {
+ readonly from?: number;
+ readonly to?: number;
+}
+
+export interface ExecutionListOptions {
+ readonly limit?: number;
+ readonly cursor?: string;
+ readonly statusFilter?: readonly ExecutionStatus[];
+ readonly triggerFilter?: readonly string[];
+ readonly toolPathFilter?: readonly string[];
+ readonly timeRange?: ExecutionTimeRange;
+ readonly after?: string;
+ readonly codeQuery?: string;
+ readonly hadElicitation?: boolean;
+ readonly sort?: ExecutionSort;
+ readonly includeMeta?: boolean;
+}
+
+export interface ExecutionListItem {
+ readonly execution: Execution;
+ readonly pendingInteraction: ExecutionInteraction | null;
+}
+
+export interface ExecutionStatusCount {
+ readonly status: ExecutionStatus;
+ readonly count: number;
+}
+
+export interface ExecutionTriggerCount {
+ readonly triggerKind: string | null;
+ readonly count: number;
+}
+
+export interface ExecutionToolFacet {
+ readonly toolPath: string;
+ readonly count: number;
+}
+
+export interface ExecutionChartBucket {
+ readonly bucketStart: number;
+ readonly counts: Readonly>;
+}
+
+export interface ExecutionInteractionCounts {
+ readonly withElicitation: number;
+ readonly withoutElicitation: number;
+}
+
+export interface ExecutionListMeta {
+ readonly totalRowCount: number;
+ readonly filterRowCount: number;
+ readonly statusCounts: readonly ExecutionStatusCount[];
+ readonly triggerCounts: readonly ExecutionTriggerCount[];
+ readonly toolFacets: readonly ExecutionToolFacet[];
+ readonly interactionCounts: ExecutionInteractionCounts;
+ readonly chartBucketMs: number;
+ readonly chartData: readonly ExecutionChartBucket[];
+}
+
+export interface ExecutionListResult {
+ readonly executions: readonly ExecutionListItem[];
+ readonly nextCursor?: string;
+ readonly meta?: ExecutionListMeta;
+}
+
+export interface ExecutionDetail {
+ readonly execution: Execution;
+ readonly pendingInteraction: ExecutionInteraction | null;
+}
+
+// ---------------------------------------------------------------------------
+// Store surface
+//
+// Exposed to callers as `executor.executions`. The engine writes on
+// every lifecycle edge (create → update → record{Interaction,ToolCall}
+// → finish). Read methods back the `/executions` HTTP API and the
+// runs UI.
+// ---------------------------------------------------------------------------
+
+export interface ExecutionStoreService {
+ readonly create: (
+ input: CreateExecutionInput,
+ ) => Effect.Effect;
+ readonly update: (
+ id: ExecutionId,
+ patch: UpdateExecutionInput,
+ ) => Effect.Effect;
+ readonly get: (
+ id: ExecutionId,
+ ) => Effect.Effect;
+ readonly list: (
+ scopeId: ScopeId,
+ options?: ExecutionListOptions,
+ ) => Effect.Effect;
+ readonly recordInteraction: (
+ input: CreateExecutionInteractionInput,
+ ) => Effect.Effect;
+ readonly resolveInteraction: (
+ id: ExecutionInteractionId,
+ patch: UpdateExecutionInteractionInput,
+ ) => Effect.Effect;
+ readonly recordToolCall: (
+ input: CreateExecutionToolCallInput,
+ ) => Effect.Effect;
+ readonly finishToolCall: (
+ id: ExecutionToolCallId,
+ patch: UpdateExecutionToolCallInput,
+ ) => Effect.Effect;
+ readonly listToolCalls: (
+ executionId: ExecutionId,
+ ) => Effect.Effect;
+ /** Drop execution rows older than the retention window. Host calls
+ * this on a schedule; the SDK doesn't drive it. */
+ readonly sweep: (
+ olderThanMs: number,
+ ) => Effect.Effect;
+}
+
+export class ExecutionStore extends Context.Tag("@executor/sdk/ExecutionStore")<
+ ExecutionStore,
+ ExecutionStoreService
+>() {}
diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts
index 80212c67e..850e83fe7 100644
--- a/packages/core/sdk/src/executor.ts
+++ b/packages/core/sdk/src/executor.ts
@@ -38,6 +38,8 @@ import {
type ElicitationHandler,
type ElicitationRequest,
} from "./elicitation";
+import { makeExecutionStore } from "./execution-store";
+import type { ExecutionStoreService } from "./executions";
import {
ConnectionNotFoundError,
ConnectionProviderNotRegisteredError,
@@ -228,6 +230,8 @@ export type Executor = {
readonly providers: () => Effect.Effect;
};
+ readonly executions: ExecutionStoreService;
+
readonly close: () => Effect.Effect;
} & PluginExtensions;
@@ -596,6 +600,11 @@ export const createExecutor = <
const adapter = buildAdapterRouter(scopedRoot);
const core = typedAdapter(adapter);
+ // Execution history — reads + writes against the generic adapter.
+ // Exposed to callers as `executor.executions`; the engine drives
+ // lifecycle transitions (create / update / recordToolCall / …).
+ const executions: ExecutionStoreService = makeExecutionStore({ core });
+
// Populated once, never mutated after startup.
const staticTools = new Map();
const staticSources = new Map();
@@ -2398,6 +2407,7 @@ export const createExecutor = <
Array.from(connectionProviders.keys()) as readonly string[],
),
},
+ executions,
close,
};
diff --git a/packages/core/sdk/src/ids.ts b/packages/core/sdk/src/ids.ts
index b62f0b395..5a3a108de 100644
--- a/packages/core/sdk/src/ids.ts
+++ b/packages/core/sdk/src/ids.ts
@@ -14,3 +14,12 @@ export type PolicyId = typeof PolicyId.Type;
export const ConnectionId = Schema.String.pipe(Schema.brand("ConnectionId"));
export type ConnectionId = typeof ConnectionId.Type;
+
+export const ExecutionId = Schema.String.pipe(Schema.brand("ExecutionId"));
+export type ExecutionId = typeof ExecutionId.Type;
+
+export const ExecutionInteractionId = Schema.String.pipe(Schema.brand("ExecutionInteractionId"));
+export type ExecutionInteractionId = typeof ExecutionInteractionId.Type;
+
+export const ExecutionToolCallId = Schema.String.pipe(Schema.brand("ExecutionToolCallId"));
+export type ExecutionToolCallId = typeof ExecutionToolCallId.Type;
diff --git a/packages/core/sdk/src/index.ts b/packages/core/sdk/src/index.ts
index d3180a8e6..40f1be9c3 100644
--- a/packages/core/sdk/src/index.ts
+++ b/packages/core/sdk/src/index.ts
@@ -23,7 +23,16 @@ export { typedAdapter } from "@executor/storage-core";
export { StorageError, UniqueViolationError } from "@executor/storage-core";
// IDs (branded)
-export { ScopeId, ToolId, SecretId, PolicyId, ConnectionId } from "./ids";
+export {
+ ScopeId,
+ ToolId,
+ SecretId,
+ PolicyId,
+ ConnectionId,
+ ExecutionId,
+ ExecutionInteractionId,
+ ExecutionToolCallId,
+} from "./ids";
// Scope
export { Scope } from "./scope";
@@ -66,6 +75,9 @@ export {
type DefinitionRow,
type SecretRow,
type ConnectionRow,
+ type ExecutionRow,
+ type ExecutionInteractionRow,
+ type ExecutionToolCallRow,
type DefinitionsInput,
type ToolAnnotations,
} from "./core-schema";
@@ -102,6 +114,41 @@ export {
type ElicitationContext,
} from "./elicitation";
+// Execution history
+export {
+ ExecutionStatus,
+ ExecutionInteractionStatus,
+ ExecutionToolCallStatus,
+ Execution,
+ ExecutionInteraction,
+ ExecutionToolCall,
+ ExecutionStore,
+ EXECUTION_STATUS_KEYS,
+ type ExecutionStoreService,
+ type CreateExecutionInput,
+ type UpdateExecutionInput,
+ type CreateExecutionInteractionInput,
+ type UpdateExecutionInteractionInput,
+ type CreateExecutionToolCallInput,
+ type UpdateExecutionToolCallInput,
+ type ExecutionListItem,
+ type ExecutionListOptions,
+ type ExecutionListResult,
+ type ExecutionListMeta,
+ type ExecutionStatusCount,
+ type ExecutionTriggerCount,
+ type ExecutionToolFacet,
+ type ExecutionInteractionCounts,
+ type ExecutionChartBucket,
+ type ExecutionTimeRange,
+ type ExecutionSort,
+ type ExecutionSortField,
+ type ExecutionSortDirection,
+ type ExecutionDetail,
+} from "./executions";
+export { makeExecutionStore } from "./execution-store";
+export { encodeCursor, decodeCursor, type CursorPayload } from "./cursor";
+
// Blob store
export {
type BlobStore,
diff --git a/packages/hosts/mcp/src/server.ts b/packages/hosts/mcp/src/server.ts
index 4c30abbb5..894ec9cc0 100644
--- a/packages/hosts/mcp/src/server.ts
+++ b/packages/hosts/mcp/src/server.ts
@@ -297,10 +297,13 @@ export const createExecutorMcpServer = (
if (supportsManagedElicitation(server)) {
const result = yield* engine.execute(code, {
onElicitation: makeMcpElicitationHandler(server, debugLog),
+ trigger: { kind: "mcp" },
});
return toMcpResult(formatExecuteResult(result));
}
- const outcome = yield* engine.executeWithPause(code);
+ const outcome = yield* engine.executeWithPause(code, {
+ trigger: { kind: "mcp" },
+ });
debugLog("execute.paused_flow_result", {
status: outcome.status,
executionId: outcome.status === "paused" ? outcome.execution.id : undefined,
diff --git a/packages/react/package.json b/packages/react/package.json
index 557aa93cd..5dc591e76 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -18,6 +18,7 @@
},
"dependencies": {
"@base-ui/react": "^1.3.0",
+ "@date-fns/utc": "^2.1.0",
"@effect-atom/atom": "^0.5.0",
"@effect-atom/atom-react": "^0.5.0",
"@effect/platform": "catalog:",
@@ -27,10 +28,12 @@
"@lobehub/icons": "^5.4.0",
"@shikijs/langs": "^4.0.2",
"@shikijs/themes": "^4.0.2",
+ "@tanstack/react-query": "^5.62.12",
"@tanstack/react-router": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "date-fns": "^3.6.0",
"effect": "catalog:",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
@@ -39,6 +42,7 @@
"react": "catalog:",
"react-day-picker": "^9.14.0",
"react-hook-form": "^7.72.0",
+ "react-hotkeys-hook": "^5.2.4",
"react-resizable-panels": "^4",
"recharts": "3.8.0",
"shiki": "^4.0.2",
diff --git a/packages/react/src/api/executions.tsx b/packages/react/src/api/executions.tsx
new file mode 100644
index 000000000..008dc0910
--- /dev/null
+++ b/packages/react/src/api/executions.tsx
@@ -0,0 +1,220 @@
+import { endOfDay, parseISO, startOfDay } from "date-fns";
+
+import { getBaseUrl } from "./base-url";
+
+// ---------------------------------------------------------------------------
+// Wire-format row projections. The server returns epoch-ms numbers for
+// every timestamp (handlers stringify/unwrap Effect Schema `Date`s at
+// the edge), so the UI works with plain numbers throughout instead of
+// reusing the SDK's Schema classes that decode to `Date`.
+// ---------------------------------------------------------------------------
+
+export type ExecutionStatus =
+ | "pending"
+ | "running"
+ | "waiting_for_interaction"
+ | "completed"
+ | "failed"
+ | "cancelled";
+
+export type Execution = {
+ readonly id: string;
+ readonly scopeId: string;
+ readonly status: ExecutionStatus;
+ readonly code: string;
+ readonly resultJson: string | null;
+ readonly errorText: string | null;
+ readonly logsJson: string | null;
+ readonly startedAt: number | null;
+ readonly completedAt: number | null;
+ readonly triggerKind: string | null;
+ readonly triggerMetaJson: string | null;
+ readonly toolCallCount: number;
+ readonly createdAt: number;
+ readonly updatedAt: number;
+};
+
+export type ExecutionInteraction = {
+ readonly id: string;
+ readonly executionId: string;
+ readonly status: "pending" | "resolved" | "cancelled";
+ readonly kind: string;
+ readonly purpose: string | null;
+ readonly payloadJson: string | null;
+ readonly responseJson: string | null;
+ readonly responsePrivateJson: string | null;
+ readonly createdAt: number;
+ readonly updatedAt: number;
+};
+
+export type ExecutionToolCall = {
+ readonly id: string;
+ readonly executionId: string;
+ readonly status: "running" | "completed" | "failed";
+ readonly toolPath: string;
+ readonly namespace: string | null;
+ readonly argsJson: string | null;
+ readonly resultJson: string | null;
+ readonly errorText: string | null;
+ readonly startedAt: number;
+ readonly completedAt: number | null;
+ readonly durationMs: number | null;
+};
+
+export type ExecutionChartBucket = {
+ readonly bucketStart: number;
+ readonly counts: Readonly>;
+};
+
+export type ExecutionListMeta = {
+ readonly totalRowCount: number;
+ readonly filterRowCount: number;
+ readonly statusCounts: ReadonlyArray<{
+ readonly status: ExecutionStatus;
+ readonly count: number;
+ }>;
+ readonly triggerCounts: ReadonlyArray<{
+ readonly triggerKind: string | null;
+ readonly count: number;
+ }>;
+ readonly toolFacets: ReadonlyArray<{
+ readonly toolPath: string;
+ readonly count: number;
+ }>;
+ readonly interactionCounts: {
+ readonly withElicitation: number;
+ readonly withoutElicitation: number;
+ };
+ readonly chartBucketMs: number;
+ readonly chartData: ReadonlyArray;
+};
+
+/**
+ * Flat list item shape consumed by the runs UI. The server returns
+ * `{ execution, pendingInteraction }` nested; we flatten here so every
+ * component can read `row.id` / `row.createdAt` / `row.pendingInteraction`
+ * without going through `.execution`.
+ */
+export type ExecutionListItem = Execution & {
+ readonly pendingInteraction: ExecutionInteraction | null;
+};
+
+export type ListExecutionsResponse = {
+ readonly executions: readonly ExecutionListItem[];
+ readonly nextCursor?: string;
+ readonly meta?: ExecutionListMeta;
+};
+
+export type GetExecutionResponse = {
+ readonly execution: Execution;
+ readonly pendingInteraction: ExecutionInteraction | null;
+};
+
+export type ListToolCallsResponse = {
+ readonly toolCalls: readonly ExecutionToolCall[];
+};
+
+type ServerListItem = {
+ readonly execution: Execution;
+ readonly pendingInteraction: ExecutionInteraction | null;
+};
+
+type ServerListResponse = {
+ readonly executions: readonly ServerListItem[];
+ readonly nextCursor?: string;
+ readonly meta?: ExecutionListMeta;
+};
+
+export type RunsQueryInput = {
+ readonly limit: number;
+ readonly cursor?: string;
+ readonly status?: string;
+ readonly trigger?: string;
+ readonly tool?: string;
+ readonly from?: string;
+ readonly to?: string;
+ /** Live-mode floor: epoch-ms. Rows strictly newer than this. */
+ readonly after?: string;
+ readonly code?: string;
+ /** Sort expression `","` e.g. `"createdAt,desc"`. */
+ readonly sort?: string;
+ /**
+ * Interactions filter: `"true"` → only runs that recorded an
+ * elicitation, `"false"` → only runs that didn't, omitted → no
+ * filter. Maps to `hadElicitation` on the server side.
+ */
+ readonly elicitation?: string;
+};
+
+const toEpochRange = (date: string | undefined, mode: "start" | "end"): number | undefined => {
+ if (!date) return undefined;
+
+ try {
+ const parsed = parseISO(date);
+ return mode === "start" ? startOfDay(parsed).getTime() : endOfDay(parsed).getTime();
+ } catch {
+ return undefined;
+ }
+};
+
+const readJson = async (response: Response): Promise => {
+ if (!response.ok) {
+ const body = await response.text().catch(() => "");
+ throw new Error(body || `Request failed with status ${response.status}`);
+ }
+
+ return (await response.json()) as T;
+};
+
+export const listExecutions = async (input: RunsQueryInput): Promise => {
+ const params = new URLSearchParams();
+ params.set("limit", String(input.limit));
+
+ if (input.cursor) params.set("cursor", input.cursor);
+ if (input.status) params.set("status", input.status);
+ if (input.trigger) params.set("trigger", input.trigger);
+ if (input.tool) params.set("tool", input.tool);
+ if (input.after) params.set("after", input.after);
+ if (input.sort) params.set("sort", input.sort);
+ if (input.elicitation) params.set("elicitation", input.elicitation);
+
+ const from = toEpochRange(input.from, "start");
+ const to = toEpochRange(input.to, "end");
+ if (from !== undefined) params.set("from", String(from));
+ if (to !== undefined) params.set("to", String(to));
+ if (input.code?.trim()) params.set("code", input.code.trim());
+
+ const response = await fetch(`${getBaseUrl()}/executions?${params.toString()}`, {
+ credentials: "include",
+ });
+
+ const payload = await readJson(response);
+ return {
+ executions: payload.executions.map(
+ (item): ExecutionListItem => ({
+ ...item.execution,
+ pendingInteraction: item.pendingInteraction,
+ }),
+ ),
+ ...(payload.nextCursor ? { nextCursor: payload.nextCursor } : {}),
+ ...(payload.meta ? { meta: payload.meta } : {}),
+ };
+};
+
+export const getExecution = async (executionId: string): Promise => {
+ const response = await fetch(`${getBaseUrl()}/executions/${executionId}`, {
+ credentials: "include",
+ });
+
+ return readJson(response);
+};
+
+export const listExecutionToolCalls = async (
+ executionId: string,
+): Promise => {
+ const response = await fetch(`${getBaseUrl()}/executions/${executionId}/tool-calls`, {
+ credentials: "include",
+ });
+
+ return readJson(response);
+};
diff --git a/packages/react/src/api/provider.tsx b/packages/react/src/api/provider.tsx
index 968c2608e..52bb940ae 100644
--- a/packages/react/src/api/provider.tsx
+++ b/packages/react/src/api/provider.tsx
@@ -1,11 +1,23 @@
import { RegistryProvider } from "@effect-atom/atom-react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as React from "react";
import { ScopeProvider } from "./scope-context";
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ refetchOnWindowFocus: true,
+ },
+ },
+});
+
export const ExecutorProvider = (
props: React.PropsWithChildren<{ fallback?: React.ReactNode }>,
) => (
-
- {props.children}
-
+
+
+ {props.children}
+
+
);
diff --git a/packages/react/src/components/runs/column-header.tsx b/packages/react/src/components/runs/column-header.tsx
new file mode 100644
index 000000000..4168d727b
--- /dev/null
+++ b/packages/react/src/components/runs/column-header.tsx
@@ -0,0 +1,100 @@
+import * as React from "react";
+import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
+
+import { cn } from "../../lib/utils";
+
+export type SortField = "createdAt" | "durationMs";
+export type SortDirection = "asc" | "desc";
+export type SortState = {
+ readonly field: SortField;
+ readonly direction: SortDirection;
+} | null;
+
+export interface RunsColumnHeaderProps {
+ readonly sort: SortState;
+ readonly onSort: (field: SortField) => void;
+ readonly visibleFields?: {
+ readonly via?: boolean;
+ readonly tools?: boolean;
+ readonly log?: boolean;
+ readonly duration_ms?: boolean;
+ };
+}
+
+export function RunsColumnHeader({ sort, onSort, visibleFields }: RunsColumnHeaderProps) {
+ const showVia = visibleFields?.via !== false;
+ const showTools = visibleFields?.tools !== false;
+ const showLog = visibleFields?.log !== false;
+ const showDuration = visibleFields?.duration_ms !== false;
+
+ return (
+
+ {/* dot column (spacer to match row layout) */}
+
+
+
+
+ status
+
+ {showVia ? via : null}
+
+ {showTools ? tools : null}
+
+ {showLog ? log : null}
+
+ {showDuration ? (
+
+ ) : null}
+
+ code
+
+ );
+}
+
+function SortHeader({
+ label,
+ field,
+ currentSort,
+ onSort,
+ className,
+}: {
+ readonly label: string;
+ readonly field: SortField;
+ readonly currentSort: SortState;
+ readonly onSort: (field: SortField) => void;
+ readonly className?: string;
+}) {
+ const isActive = currentSort?.field === field;
+ const direction = isActive ? currentSort.direction : null;
+ const Icon = direction === "desc" ? ArrowDown : direction === "asc" ? ArrowUp : ArrowUpDown;
+
+ return (
+ // oxlint-disable-next-line react/forbid-elements -- column headers are dense table-level affordances;