diff --git a/CHANGELOG.md b/CHANGELOG.md index f29b7b8ae8..5b4a02f43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # Changelog +## [1.532.0](https://github.com/windmill-labs/windmill/compare/v1.531.0...v1.532.0) (2025-08-22) + + +### Features + +* **aichat:** allow adding contexts to flow mode ([#6424](https://github.com/windmill-labs/windmill/issues/6424)) ([73272f1](https://github.com/windmill-labs/windmill/commit/73272f16fddc355703b04f2c3458520753d1e19c)) +* json schema resource ([#6433](https://github.com/windmill-labs/windmill/issues/6433)) ([7da79a8](https://github.com/windmill-labs/windmill/commit/7da79a8bc525fc6b89748ad0af25c2bac4ca2ef3)) + +## [1.531.0](https://github.com/windmill-labs/windmill/compare/v1.530.0...v1.531.0) (2025-08-22) + + +### Features + +* ai agent steps ([#6393](https://github.com/windmill-labs/windmill/issues/6393)) ([958e8af](https://github.com/windmill-labs/windmill/commit/958e8af78290cf859f98c45c012ed41e3bada39e)) +* bump Go version from 1.22.0 to 1.25.0 [#6415](https://github.com/windmill-labs/windmill/issues/6415) ([c92bfe6](https://github.com/windmill-labs/windmill/commit/c92bfe6601fd96f6d74860f52f9307e02961ac21)) + + +### Bug Fixes + +* **app:** fix ctrl drag for insertion into subgrids ([51ea947](https://github.com/windmill-labs/windmill/commit/51ea9473ef23c6871699e69bbe79772a4d50d3b8)) +* **frontend:** graph cache of ai agent step tools ([#6431](https://github.com/windmill-labs/windmill/issues/6431)) ([28f1d61](https://github.com/windmill-labs/windmill/commit/28f1d611643459d42531fa217c185408eb97d6d1)) +* make relevant sidebar menu items a instead of button ([06d078e](https://github.com/windmill-labs/windmill/commit/06d078ebfa8f70b66bc764eae70d33c8c57b4012)) +* s3 result presigned not working with list ([9df008b](https://github.com/windmill-labs/windmill/commit/9df008b9f8fe58692463e4b9da0538935e458b10)) + +## [1.530.0](https://github.com/windmill-labs/windmill/compare/v1.529.0...v1.530.0) (2025-08-20) + + +### Features + +* **mcp:** add script preview testing tool ([#6417](https://github.com/windmill-labs/windmill/issues/6417)) ([ae49737](https://github.com/windmill-labs/windmill/commit/ae497376769f5cd49a41c22cf558a9f052d5b56e)) + + +### Bug Fixes + +* aggrid newchange to point to correct idx ([#6425](https://github.com/windmill-labs/windmill/issues/6425)) ([511ff5e](https://github.com/windmill-labs/windmill/commit/511ff5e9f794c29c3c8a5fc0480675a1258fb056)) +* fix preprocessor preview ([47e49b2](https://github.com/windmill-labs/windmill/commit/47e49b243d8cf6d29fa5a59918a2168b87111352)) +* improve flow editor log streaming for individual tests ([ac066ab](https://github.com/windmill-labs/windmill/commit/ac066abb980501577cc96330a4a5f1309aa35661)) + +## [1.529.0](https://github.com/windmill-labs/windmill/compare/v1.528.0...v1.529.0) (2025-08-19) + + +### Features + +* add prometheus metric queue_running_count ([#6413](https://github.com/windmill-labs/windmill/issues/6413)) ([49ed757](https://github.com/windmill-labs/windmill/commit/49ed7574245784681f800199b6c7a47df5788e45)) +* **aichat:** add tool to test specific module in flow mode ([#6381](https://github.com/windmill-labs/windmill/issues/6381)) ([dfb32d2](https://github.com/windmill-labs/windmill/commit/dfb32d2949541ed149722cff88918d7c6e3dc307)) +* **frontend:** add relative line numbers toggle ([#6416](https://github.com/windmill-labs/windmill/issues/6416)) ([4349a20](https://github.com/windmill-labs/windmill/commit/4349a2024da2aa7406b16254fbfc427526718903)) + + +### Bug Fixes + +* **cli:** pass HEADERS environment variable to fetch calls in generate-locks ([#6422](https://github.com/windmill-labs/windmill/issues/6422)) ([7f11eb9](https://github.com/windmill-labs/windmill/commit/7f11eb98b5682511e05c270d7ba860e3e0db61e9)) +* improve computeAssetNodes rendering caching and performance ([#6414](https://github.com/windmill-labs/windmill/issues/6414)) ([51568ee](https://github.com/windmill-labs/windmill/commit/51568eee025eab6e0069a8e719a562b721fc8c43)) + +## [1.528.0](https://github.com/windmill-labs/windmill/compare/v1.527.1...v1.528.0) (2025-08-19) + + +### Features + +* native k8s autoscaling integration (EE) ([#6405](https://github.com/windmill-labs/windmill/issues/6405)) ([eaf4054](https://github.com/windmill-labs/windmill/commit/eaf4054bd380101856c02a0d07430ff3a0180880)) + + +### Bug Fixes + +* flow status reactivity improvement ([#6402](https://github.com/windmill-labs/windmill/issues/6402)) ([5e73c49](https://github.com/windmill-labs/windmill/commit/5e73c49ab670be0f55794f5d0cb182de9efd500a)) + ## [1.527.1](https://github.com/windmill-labs/windmill/compare/v1.527.0...v1.527.1) (2025-08-16) diff --git a/CLAUDE.md b/CLAUDE.md index f61e0336be..3f29bf04ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,17 @@ Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details. +## New Feature Implementation Guidelines + +When implementing new features in Windmill, follow these best practices: + +- **Clean Code First**: Write clean, readable, and maintainable code. Prioritize clarity over cleverness. +- **Avoid Duplication at All Costs**: Before writing new code, thoroughly search for existing implementations that can be reused or extended. +- **Adapt Existing Code**: Refactor and generalize existing code when necessary to avoid logic duplication. Extract common patterns into reusable utilities. +- **Follow Established Patterns**: Study existing code patterns in the codebase and maintain consistency with established conventions. +- **Single Responsibility**: Each function, component, and module should have a single, well-defined responsibility. +- **Incremental Implementation**: Break large features into smaller, reviewable chunks that can be implemented and tested incrementally. + ## Language-Specific Guides - Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt diff --git a/Dockerfile b/Dockerfile index 3815744d1e..3d56c251f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,7 +90,7 @@ ARG POWERSHELL_VERSION=7.5.0 ARG POWERSHELL_DEB_VERSION=7.5.0-1 ARG KUBECTL_VERSION=1.28.7 ARG HELM_VERSION=3.14.3 -ARG GO_VERSION=1.22.5 +ARG GO_VERSION=1.25.0 ARG APP=/usr/src/app ARG WITH_POWERSHELL=true ARG WITH_KUBECTL=true diff --git a/backend/.sqlx/query-acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597.json b/backend/.sqlx/query-193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b.json similarity index 94% rename from backend/.sqlx/query-acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597.json rename to backend/.sqlx/query-193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b.json index fbd4571adc..2d4df84899 100644 --- a/backend/.sqlx/query-acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597.json +++ b/backend/.sqlx/query-193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)", + "query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)", "describe": { "columns": [], "parameters": { @@ -39,7 +39,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } @@ -121,10 +122,11 @@ ] } } - } + }, + "Bool" ] }, "nullable": [] }, - "hash": "acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597" + "hash": "193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b" } diff --git a/backend/.sqlx/query-1b56a720d99a689e80d12ee1efbfeb71d7cd7bb17e936746749c958062cdff9e.json b/backend/.sqlx/query-1b56a720d99a689e80d12ee1efbfeb71d7cd7bb17e936746749c958062cdff9e.json new file mode 100644 index 0000000000..f9cb3add9e --- /dev/null +++ b/backend/.sqlx/query-1b56a720d99a689e80d12ee1efbfeb71d7cd7bb17e936746749c958062cdff9e.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT tag AS \"tag!\", count(*) AS \"count!\" FROM v2_job_queue WHERE\n running = true\n GROUP BY tag", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "tag!", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + null + ] + }, + "hash": "1b56a720d99a689e80d12ee1efbfeb71d7cd7bb17e936746749c958062cdff9e" +} diff --git a/backend/.sqlx/query-3e3afba04a10f16606e17cea6b31d9578cac41b76092722fd2698afb4cf08834.json b/backend/.sqlx/query-3e3afba04a10f16606e17cea6b31d9578cac41b76092722fd2698afb4cf08834.json new file mode 100644 index 0000000000..d49747b7be --- /dev/null +++ b/backend/.sqlx/query-3e3afba04a10f16606e17cea6b31d9578cac41b76092722fd2698afb4cf08834.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $2::TEXT, 'agent_actions_success'],\n COALESCE(\n flow_status->'modules'->$2->'agent_actions_success',\n to_jsonb(ARRAY[]::bool[])\n ) || to_jsonb(ARRAY[$3::bool])\n )\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "3e3afba04a10f16606e17cea6b31d9578cac41b76092722fd2698afb4cf08834" +} diff --git a/backend/.sqlx/query-3f08ffbb5c71b873a9e164ecb0b10fffb37599f2a703885ee723cb9290fed13e.json b/backend/.sqlx/query-3f08ffbb5c71b873a9e164ecb0b10fffb37599f2a703885ee723cb9290fed13e.json index 9f5e2e9d8f..5c5d9b68ba 100644 --- a/backend/.sqlx/query-3f08ffbb5c71b873a9e164ecb0b10fffb37599f2a703885ee723cb9290fed13e.json +++ b/backend/.sqlx/query-3f08ffbb5c71b873a9e164ecb0b10fffb37599f2a703885ee723cb9290fed13e.json @@ -28,7 +28,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-488dd591096b2b47787afdc3a1d73917ed13269f2ee20b86df79fca2c8efe672.json b/backend/.sqlx/query-488dd591096b2b47787afdc3a1d73917ed13269f2ee20b86df79fca2c8efe672.json index 5f59450dc3..af477b4db6 100644 --- a/backend/.sqlx/query-488dd591096b2b47787afdc3a1d73917ed13269f2ee20b86df79fca2c8efe672.json +++ b/backend/.sqlx/query-488dd591096b2b47787afdc3a1d73917ed13269f2ee20b86df79fca2c8efe672.json @@ -40,7 +40,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-4aaab98ebdaa90f1edf49ac96fba6c391c4d0054a618b861464ee37239f1f1e0.json b/backend/.sqlx/query-4aaab98ebdaa90f1edf49ac96fba6c391c4d0054a618b861464ee37239f1f1e0.json index e16cd7db75..fa33c3b8e0 100644 --- a/backend/.sqlx/query-4aaab98ebdaa90f1edf49ac96fba6c391c4d0054a618b861464ee37239f1f1e0.json +++ b/backend/.sqlx/query-4aaab98ebdaa90f1edf49ac96fba6c391c4d0054a618b861464ee37239f1f1e0.json @@ -68,7 +68,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-7274e9489b18d7ab82bad1fbff89ffe41d162b482adb04b7b898a19576af2a5e.json b/backend/.sqlx/query-7274e9489b18d7ab82bad1fbff89ffe41d162b482adb04b7b898a19576af2a5e.json index 5783969e29..63363e74ff 100644 --- a/backend/.sqlx/query-7274e9489b18d7ab82bad1fbff89ffe41d162b482adb04b7b898a19576af2a5e.json +++ b/backend/.sqlx/query-7274e9489b18d7ab82bad1fbff89ffe41d162b482adb04b7b898a19576af2a5e.json @@ -98,7 +98,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } @@ -148,7 +149,8 @@ "oracledb", "nu", "java", - "duckdb" + "duckdb", + "ruby" ] } } diff --git a/backend/.sqlx/query-7af1cf089022fc1c3597b270b69aa669a153ab0c0bb2807cd4f7fd405afa6f69.json b/backend/.sqlx/query-7af1cf089022fc1c3597b270b69aa669a153ab0c0bb2807cd4f7fd405afa6f69.json index d2e54f45c8..e4a0fec1b2 100644 --- a/backend/.sqlx/query-7af1cf089022fc1c3597b270b69aa669a153ab0c0bb2807cd4f7fd405afa6f69.json +++ b/backend/.sqlx/query-7af1cf089022fc1c3597b270b69aa669a153ab0c0bb2807cd4f7fd405afa6f69.json @@ -38,7 +38,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5.json b/backend/.sqlx/query-805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5.json new file mode 100644 index 0000000000..5500f83561 --- /dev/null +++ b/backend/.sqlx/query-805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5.json @@ -0,0 +1,61 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT runnable_id as \"runnable_id: ScriptHash\", raw_flow as \"raw_flow: _\", kind as \"kind: _\" FROM v2_job WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "runnable_id: ScriptHash", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "raw_flow: _", + "type_info": "Jsonb" + }, + { + "ordinal": 2, + "name": "kind: _", + "type_info": { + "Custom": { + "name": "job_kind", + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies", + "http", + "graphql", + "postgresql", + "noop", + "appdependencies", + "deploymentcallback", + "singlescriptflow", + "flowscript", + "flownode", + "appscript", + "aiagent" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true, + true, + false + ] + }, + "hash": "805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5" +} diff --git a/backend/.sqlx/query-92c7c961198e506426bf3f97a8ddbb34af450041c675b30b708fed3ef9e01d2d.json b/backend/.sqlx/query-92c7c961198e506426bf3f97a8ddbb34af450041c675b30b708fed3ef9e01d2d.json index fcb9657c8a..bcc10fcf4a 100644 --- a/backend/.sqlx/query-92c7c961198e506426bf3f97a8ddbb34af450041c675b30b708fed3ef9e01d2d.json +++ b/backend/.sqlx/query-92c7c961198e506426bf3f97a8ddbb34af450041c675b30b708fed3ef9e01d2d.json @@ -28,7 +28,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-9a923c85a015e4149328f650e77872a1c39d5992efd6abd2d3b8a558d7b884a1.json b/backend/.sqlx/query-9a923c85a015e4149328f650e77872a1c39d5992efd6abd2d3b8a558d7b884a1.json new file mode 100644 index 0000000000..0b6f4619c8 --- /dev/null +++ b/backend/.sqlx/query-9a923c85a015e4149328f650e77872a1c39d5992efd6abd2d3b8a558d7b884a1.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $3::TEXT, 'agent_actions'],\n $2\n )\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Jsonb", + "Text" + ] + }, + "nullable": [] + }, + "hash": "9a923c85a015e4149328f650e77872a1c39d5992efd6abd2d3b8a558d7b884a1" +} diff --git a/backend/.sqlx/query-a6608d47b96d851eb7b04d2e4b472889ff257db0f0e3e9252adda2e4ef2039d6.json b/backend/.sqlx/query-a6608d47b96d851eb7b04d2e4b472889ff257db0f0e3e9252adda2e4ef2039d6.json index 5f6611dae6..8fea4c40ec 100644 --- a/backend/.sqlx/query-a6608d47b96d851eb7b04d2e4b472889ff257db0f0e3e9252adda2e4ef2039d6.json +++ b/backend/.sqlx/query-a6608d47b96d851eb7b04d2e4b472889ff257db0f0e3e9252adda2e4ef2039d6.json @@ -98,7 +98,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json b/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json index 072bc63b66..e07a3ccdd7 100644 --- a/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json +++ b/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json @@ -35,7 +35,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-b3c02fd225a6aa78785d466e7f033b38deb9c7fa17bd3836c9ad8884f27be84a.json b/backend/.sqlx/query-b3c02fd225a6aa78785d466e7f033b38deb9c7fa17bd3836c9ad8884f27be84a.json index 4417c453f9..5a6aa7dd69 100644 --- a/backend/.sqlx/query-b3c02fd225a6aa78785d466e7f033b38deb9c7fa17bd3836c9ad8884f27be84a.json +++ b/backend/.sqlx/query-b3c02fd225a6aa78785d466e7f033b38deb9c7fa17bd3836c9ad8884f27be84a.json @@ -33,7 +33,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json index 54e94cfb8f..99269c9851 100644 --- a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json +++ b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json @@ -18,8 +18,8 @@ "Left": [] }, "nullable": [ - false, - true + true, + false ] }, "hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76" diff --git a/backend/.sqlx/query-d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5.json b/backend/.sqlx/query-d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5.json new file mode 100644 index 0000000000..dbd97c765a --- /dev/null +++ b/backend/.sqlx/query-d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT value from resource WHERE path = $1 AND workspace_id = $2 AND resource_type = 'json_schema'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "value", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5" +} diff --git a/backend/.sqlx/query-f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7.json b/backend/.sqlx/query-f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7.json deleted file mode 100644 index fb865ca894..0000000000 --- a/backend/.sqlx/query-f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n cj.id AS \"id!\",\n cj.workspace_id AS \"workspace_id!\",\n cj.parent_job,\n cj.created_by AS \"created_by!\",\n cj.duration_ms AS \"duration_ms!\",\n cj.success AS \"success!\",\n cj.script_hash AS \"script_hash!: Option\",\n cj.script_path,\n cj.args AS \"args: sqlx::types::Json>>\",\n cj.result AS \"result: sqlx::types::Json>\",\n cj.deleted AS \"deleted!\",\n cj.canceled AS \"canceled!\",\n cj.canceled_by,\n cj.canceled_reason,\n cj.job_kind AS \"job_kind!: JobKind\",\n cj.schedule_path,\n cj.permissioned_as AS \"permissioned_as!\",\n cj.is_flow_step AS \"is_flow_step!\",\n cj.language AS \"language: ScriptLang\",\n cj.is_skipped AS \"is_skipped!\",\n cj.email AS \"email!\",\n cj.visible_to_owner AS \"visible_to_owner!\",\n cj.mem_peak,\n cj.tag AS \"tag!\",\n cj.created_at AS \"created_at!\",\n cj.started_at,\n job_logs.logs,\n job_logs.log_offset AS \"log_offset?\",\n job_logs.log_file_index\n\n FROM v2_as_completed_job AS cj\n LEFT JOIN job_logs ON cj.id = job_logs.job_id\n WHERE cj.created_at < $1\n ORDER BY cj.created_at ASC LIMIT $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id!", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "workspace_id!", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "parent_job", - "type_info": "Uuid" - }, - { - "ordinal": 3, - "name": "created_by!", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "duration_ms!", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "success!", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "script_hash!: Option", - "type_info": "Int8" - }, - { - "ordinal": 7, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "args: sqlx::types::Json>>", - "type_info": "Jsonb" - }, - { - "ordinal": 9, - "name": "result: sqlx::types::Json>", - "type_info": "Jsonb" - }, - { - "ordinal": 10, - "name": "deleted!", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "canceled!", - "type_info": "Bool" - }, - { - "ordinal": 12, - "name": "canceled_by", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "canceled_reason", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "job_kind!: JobKind", - "type_info": { - "Custom": { - "name": "job_kind", - "kind": { - "Enum": [ - "script", - "preview", - "flow", - "dependencies", - "flowpreview", - "script_hub", - "identity", - "flowdependencies", - "http", - "graphql", - "postgresql", - "noop", - "appdependencies", - "deploymentcallback", - "singlescriptflow", - "flowscript", - "flownode", - "appscript" - ] - } - } - } - }, - { - "ordinal": 15, - "name": "schedule_path", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "permissioned_as!", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "is_flow_step!", - "type_info": "Bool" - }, - { - "ordinal": 18, - "name": "language: ScriptLang", - "type_info": { - "Custom": { - "name": "script_lang", - "kind": { - "Enum": [ - "python3", - "deno", - "go", - "bash", - "postgresql", - "nativets", - "bun", - "mysql", - "bigquery", - "snowflake", - "graphql", - "powershell", - "mssql", - "php", - "bunnative", - "rust", - "ansible", - "csharp", - "oracledb", - "nu", - "java", - "duckdb", - "ruby" - ] - } - } - } - }, - { - "ordinal": 19, - "name": "is_skipped!", - "type_info": "Bool" - }, - { - "ordinal": 20, - "name": "email!", - "type_info": "Varchar" - }, - { - "ordinal": 21, - "name": "visible_to_owner!", - "type_info": "Bool" - }, - { - "ordinal": 22, - "name": "mem_peak", - "type_info": "Int4" - }, - { - "ordinal": 23, - "name": "tag!", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "created_at!", - "type_info": "Timestamptz" - }, - { - "ordinal": 25, - "name": "started_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 26, - "name": "logs", - "type_info": "Text" - }, - { - "ordinal": 27, - "name": "log_offset?", - "type_info": "Int4" - }, - { - "ordinal": 28, - "name": "log_file_index", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Timestamptz", - "Int8" - ] - }, - "nullable": [ - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - false, - true - ] - }, - "hash": "f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7" -} diff --git a/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json b/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json index b100feb47d..0e42bd0fdb 100644 --- a/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json +++ b/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json @@ -35,7 +35,8 @@ "singlescriptflow", "flowscript", "flownode", - "appscript" + "appscript", + "aiagent" ] } } diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 6eecebb1e6..51a227cf4e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -465,7 +465,7 @@ version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7686986a3bf2254c9fb130c623cdcb2f8e1f15763e7c71c310f0834da3d292" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", ] [[package]] @@ -559,6 +559,18 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -1012,7 +1024,7 @@ dependencies = [ "http 1.3.1", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.24.2", "hyper-rustls 0.27.7", "hyper-util", @@ -1080,9 +1092,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.7" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d52251ed4b9776a3e8487b2a01ac915f73b2da3af8fc1e77e0fce697a550d4" +checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1167,7 +1179,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "itoa", "matchit", @@ -1334,7 +1346,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cexpr", "clang-sys", "itertools 0.12.1", @@ -1357,7 +1369,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1409,9 +1421,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" dependencies = [ "serde", ] @@ -1523,7 +1535,7 @@ dependencies = [ "hex", "http 1.3.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-named-pipe", "hyper-util", "hyperlocal", @@ -1534,7 +1546,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-util", "tower-service", @@ -1555,9 +1567,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a0c21249ad725ebcadcb1b1885f8e3d56e8e6b8924f560268aab000982d637" +checksum = "537c317ddf588aab15c695bf92cf55dec159b93221c074180ca3e0e5a94da415" dependencies = [ "bon-macros", "rustversion", @@ -1565,9 +1577,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a660ebdea4d4d3ec7788cfc9c035b66efb66028b9b97bf6cde7023ccc8e77e28" +checksum = "ca5abbf2d4a4c6896197c9de13d6d7cb7eff438c63dacde1dde980569cb00248" dependencies = [ "darling 0.21.2", "ident_case", @@ -1810,7 +1822,7 @@ dependencies = [ "byteorder", "gemm 0.17.1", "half", - "memmap2 0.9.7", + "memmap2 0.9.8", "num-traits", "num_cpus", "rand 0.9.0", @@ -1905,9 +1917,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -1931,9 +1943,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -2489,7 +2501,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "libloading 0.8.8", "winapi", ] @@ -3324,7 +3336,7 @@ dependencies = [ "swc_visit", "swc_visit_macros", "text_lines", - "thiserror 2.0.14", + "thiserror 2.0.16", "unicode-width 0.1.14", "url", ] @@ -3338,7 +3350,7 @@ dependencies = [ "async-trait", "deno_core", "deno_error", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "uuid", ] @@ -3355,7 +3367,7 @@ dependencies = [ "rusqlite", "serde", "sha2 0.10.9", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", ] @@ -3399,7 +3411,7 @@ dependencies = [ "deno_webgpu", "image", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -3425,7 +3437,7 @@ dependencies = [ "serde", "serde_json", "sys_traits", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -3470,7 +3482,7 @@ dependencies = [ "smallvec", "sourcemap 8.0.1", "static_assertions", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "url", "v8", @@ -3495,7 +3507,7 @@ dependencies = [ "deno_core", "deno_error", "saffron", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", ] @@ -3533,7 +3545,7 @@ dependencies = [ "sha2 0.10.9", "signature", "spki", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "uuid", "x25519-dalek", @@ -3585,7 +3597,7 @@ dependencies = [ "hickory-resolver", "http 1.3.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.27.7", "hyper-util", "ipnet", @@ -3593,7 +3605,7 @@ dependencies = [ "rustls-webpki 0.102.8", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-rustls 0.26.2", "tokio-socks", @@ -3621,7 +3633,7 @@ dependencies = [ "serde", "serde-value", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "winapi", ] @@ -3647,7 +3659,7 @@ dependencies = [ "rand 0.8.5", "rayon", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "winapi", "windows-sys 0.59.0", ] @@ -3673,7 +3685,7 @@ dependencies = [ "http 1.3.1", "httparse", "hyper 0.14.32", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "itertools 0.10.5", "memmem", @@ -3686,7 +3698,7 @@ dependencies = [ "scopeguard", "serde", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-util", ] @@ -3746,7 +3758,7 @@ dependencies = [ "rand 0.8.5", "rusqlite", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -3759,7 +3771,7 @@ dependencies = [ "deno_semver", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -3786,7 +3798,7 @@ dependencies = [ "libloading 0.7.4", "log", "napi_sym", - "thiserror 2.0.14", + "thiserror 2.0.16", "windows-sys 0.59.0", ] @@ -3820,7 +3832,7 @@ dependencies = [ "rustls-tokio-stream", "serde", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", ] @@ -3866,7 +3878,7 @@ dependencies = [ "hkdf", "http 1.3.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "idna", "indexmap 2.10.0", @@ -3907,7 +3919,7 @@ dependencies = [ "spki", "stable_deref_trait", "sys_traits", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-eld", "url", @@ -3935,7 +3947,7 @@ dependencies = [ "monch", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -3953,7 +3965,7 @@ dependencies = [ "strum 0.25.0", "strum_macros 0.25.3", "syn 2.0.106", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -3974,7 +3986,7 @@ dependencies = [ "serde", "signal-hook", "signal-hook-registry", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "winapi", ] @@ -3993,7 +4005,7 @@ dependencies = [ "serde", "serde_json", "sys_traits", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -4006,7 +4018,7 @@ dependencies = [ "deno_error", "percent-encoding", "sys_traits", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -4027,7 +4039,7 @@ dependencies = [ "once_cell", "percent-encoding", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "which 6.0.3", "winapi", ] @@ -4054,7 +4066,7 @@ dependencies = [ "serde", "simd-json", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "which 6.0.3", "winapi", @@ -4088,7 +4100,7 @@ dependencies = [ "once_cell", "parking_lot 0.12.4", "sys_traits", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -4137,7 +4149,7 @@ dependencies = [ "http 1.3.1", "http-body-util", "hyper 0.14.32", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "libc", "log", @@ -4153,7 +4165,7 @@ dependencies = [ "serde", "sys_traits", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-metrics", "twox-hash 1.6.3", @@ -4176,7 +4188,7 @@ dependencies = [ "monch", "once_cell", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -4191,7 +4203,7 @@ dependencies = [ "deno_error", "deno_tls", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.27.7", "hyper-util", "log", @@ -4203,7 +4215,7 @@ dependencies = [ "opentelemetry_sdk", "pin-project", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", ] @@ -4231,7 +4243,7 @@ dependencies = [ "rustls-tokio-stream", "rustls-webpki 0.102.8", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "webpki-roots 0.26.11", ] @@ -4255,7 +4267,7 @@ checksum = "d79e743ad841f7826d46c6944580f5ba665fe9ab4c31a68c4eed8b5a78225da3" dependencies = [ "deno_core", "deno_error", - "thiserror 2.0.14", + "thiserror 2.0.16", "urlpattern", ] @@ -4275,7 +4287,7 @@ dependencies = [ "flate2", "futures", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "uuid", ] @@ -4290,7 +4302,7 @@ dependencies = [ "deno_error", "raw-window-handle", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "wgpu-core", "wgpu-types", @@ -4321,12 +4333,12 @@ dependencies = [ "h2 0.4.12", "http 1.3.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "once_cell", "rustls-tokio-stream", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", ] @@ -4340,7 +4352,7 @@ dependencies = [ "deno_error", "deno_web", "rusqlite", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -4388,7 +4400,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-util", "url", @@ -4413,7 +4425,7 @@ dependencies = [ "rand 0.8.5", "rusqlite", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-stream", "uuid", @@ -4522,6 +4534,26 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "des" version = "0.7.0" @@ -4892,6 +4924,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "either" version = "1.15.0" @@ -4958,6 +5002,26 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -5053,6 +5117,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -5123,7 +5197,7 @@ dependencies = [ "base64 0.21.7", "bytes", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "pin-project", "rand 0.8.5", @@ -5178,14 +5252,14 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5200,7 +5274,7 @@ version = "25.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "rustc_version 0.4.1", ] @@ -5293,9 +5367,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -5746,9 +5820,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" dependencies = [ "cc", "cfg-if", @@ -6020,7 +6094,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "gpu-alloc-types", ] @@ -6030,7 +6104,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", ] [[package]] @@ -6039,7 +6113,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -6050,7 +6124,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", ] [[package]] @@ -6208,6 +6282,30 @@ dependencies = [ "num-traits", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http 1.3.1", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.3.1", +] + [[package]] name = "heck" version = "0.4.1" @@ -6256,7 +6354,7 @@ dependencies = [ "reqwest 0.12.23", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "ureq", "windows-sys 0.60.2", @@ -6281,7 +6379,7 @@ dependencies = [ "once_cell", "rand 0.9.0", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "tinyvec", "tokio", "tracing", @@ -6305,7 +6403,7 @@ dependencies = [ "resolv-conf", "serde", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -6348,6 +6446,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "hstr" version = "0.2.17" @@ -6468,13 +6577,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", @@ -6482,11 +6592,32 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", ] +[[package]] +name = "hyper-http-proxy" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http 1.3.1", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls-native-certs 0.7.3", + "tokio", + "tokio-rustls 0.26.2", + "tower-service", +] + [[package]] name = "hyper-named-pipe" version = "0.1.0" @@ -6494,7 +6625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "pin-project-lite", "tokio", @@ -6525,8 +6656,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", + "log", "rustls 0.23.29", "rustls-native-certs 0.8.1", "rustls-pki-types", @@ -6542,7 +6674,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "pin-project-lite", "tokio", @@ -6570,7 +6702,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -6591,7 +6723,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", @@ -6612,7 +6744,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "pin-project-lite", "tokio", @@ -6746,9 +6878,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -6813,7 +6945,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -6902,9 +7034,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] @@ -6915,7 +7047,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cfg-if", "libc", ] @@ -7058,6 +7190,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159294d661a039f7644cea7e4d844e6b25aaf71c1ffe9d73a96d768c24b0faf4" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "jsonc-parser" version = "0.26.3" @@ -7067,6 +7211,29 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonpath-rust" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -7120,6 +7287,18 @@ dependencies = [ "signature", ] +[[package]] +name = "k8s-openapi" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa60a41b57ae1a0a071af77dbcf89fc9819cfe66edaf2beeb204c34459dcf0b2" +dependencies = [ + "base64 0.22.1", + "chrono", + "serde", + "serde_json", +] + [[package]] name = "keccak" version = "0.1.5" @@ -7190,6 +7369,116 @@ dependencies = [ "libc", ] +[[package]] +name = "kube" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778f98664beaf4c3c11372721e14310d1ae00f5e2d9aabcf8906c881aa4e9f51" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb276b85b6e94ded00ac8ea2c68fcf4697ea0553cb25fddc35d4a0ab718db8d" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "either", + "futures", + "home", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-http-proxy", + "hyper-rustls 0.27.7", + "hyper-timeout", + "hyper-util", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem 3.0.5", + "rustls 0.23.29", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.16", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c56ff45deb0031f2a476017eed60c06872251f271b8387ad8020b8fef60960" +dependencies = [ + "chrono", + "derive_more 2.0.1", + "form_urlencoded", + "http 1.3.1", + "json-patch", + "k8s-openapi", + "schemars 0.8.22", + "serde", + "serde-value", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] +name = "kube-derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079fc8c1c397538628309cfdee20696ebdcc26745f9fb17f89b78782205bd995" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.106", +] + +[[package]] +name = "kube-runtime" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f1326e946fadf6248febdf8a1c001809c3899ccf48cb9768cbc536b741040dc" +dependencies = [ + "ahash 0.8.12", + "async-broadcast", + "async-stream", + "backon", + "educe", + "futures", + "hashbrown 0.15.5", + "hostname", + "json-patch", + "k8s-openapi", + "kube-client", + "parking_lot 0.12.4", + "pin-project", + "serde", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "lalrpop-util" version = "0.20.2" @@ -7387,7 +7676,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "libc", "redox_syscall 0.5.17", ] @@ -7675,7 +7964,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17703a19c80bbdd0b7919f0f104f3b0597f7de4fc4e90a477c15366a5ba03faa" dependencies = [ - "derive_more", + "derive_more 0.99.20", "malachite", "num-integer", "num-traits", @@ -7796,9 +8085,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", "stable_deref_trait", @@ -7825,7 +8114,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -8026,7 +8315,7 @@ dependencies = [ "quote", "syn 2.0.106", "termcolor", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -8051,7 +8340,7 @@ dependencies = [ "serde", "serde_json", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-native-tls", "tokio-util", @@ -8066,7 +8355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb9f371618ce723f095c61fbcdc36e8936956d2b62832f9c7648689b338e052" dependencies = [ "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.9.2", "btoi", "byteorder", "bytes", @@ -8083,7 +8372,7 @@ dependencies = [ "serde_json", "sha1", "sha2 0.10.9", - "thiserror 2.0.14", + "thiserror 2.0.16", "uuid", ] @@ -8095,7 +8384,7 @@ checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ "arrayvec", "bit-set 0.5.3", - "bitflags 2.9.1", + "bitflags 2.9.2", "codespan-reporting", "hexf-parse", "indexmap 2.10.0", @@ -8187,7 +8476,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cfg-if", "libc", ] @@ -8198,7 +8487,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -8210,7 +8499,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -8253,7 +8542,7 @@ dependencies = [ "serde", "serde_json", "sys_traits", - "thiserror 2.0.14", + "thiserror 2.0.16", "url", ] @@ -8292,7 +8581,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "crossbeam-channel", "filetime", "fsevent-sys", @@ -8420,7 +8709,7 @@ dependencies = [ "num-format", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "typetag", "windows-sys 0.48.0", ] @@ -8674,7 +8963,7 @@ dependencies = [ "http-body-util", "httparse", "humantime", - "hyper 1.6.0", + "hyper 1.7.0", "itertools 0.14.0", "md-5 0.10.6", "parking_lot 0.12.4", @@ -8687,7 +8976,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", "url", @@ -8744,7 +9033,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "libc", "once_cell", "onig_sys", @@ -8814,7 +9103,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cfg-if", "foreign-types 0.3.2", "libc", @@ -9267,9 +9556,53 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror 2.0.16", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +dependencies = [ + "pest", + "sha2 0.10.9", +] [[package]] name = "petgraph" @@ -9596,9 +9929,9 @@ checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn 2.0.106", @@ -9693,9 +10026,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -9720,7 +10053,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "chrono", "flate2", "hex", @@ -9734,7 +10067,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "chrono", "hex", ] @@ -9770,7 +10103,7 @@ dependencies = [ "lazy_static", "memchr", "parking_lot 0.12.4", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -9860,7 +10193,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "getopts", "memchr", "unicase", @@ -9954,7 +10287,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls 0.23.29", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -9975,7 +10308,7 @@ dependencies = [ "rustls 0.23.29", "rustls-pki-types", "slab", - "thiserror 2.0.14", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -10127,7 +10460,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", ] [[package]] @@ -10249,7 +10582,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", ] [[package]] @@ -10271,7 +10604,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -10417,7 +10750,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.27.7", "hyper-tls 0.6.0", "hyper-util", @@ -10477,7 +10810,7 @@ dependencies = [ "futures", "getrandom 0.2.16", "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "parking_lot 0.11.2", "reqwest 0.12.23", "reqwest-middleware", @@ -10601,7 +10934,7 @@ dependencies = [ "serde", "serde_json", "sse-stream", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-util", @@ -10630,7 +10963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.9.1", + "bitflags 2.9.2", "serde", "serde_derive", ] @@ -10681,7 +11014,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "fallible-iterator 0.3.0", "fallible-streaming-iterator", "hashlink 0.9.1", @@ -10802,7 +11135,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "errno", "libc", "linux-raw-sys 0.4.15", @@ -10815,7 +11148,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "errno", "libc", "linux-raw-sys 0.9.4", @@ -11043,7 +11376,7 @@ version = "13.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "cfg-if", "clipboard-win", "fd-lock", @@ -11255,13 +11588,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -11274,7 +11616,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -11393,9 +11735,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "indexmap 2.10.0", "itoa", @@ -11474,7 +11816,7 @@ dependencies = [ "num-bigint", "serde", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.16", "v8", ] @@ -11510,6 +11852,19 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.10.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serde_yml" version = "0.0.12" @@ -11689,7 +12044,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.14", + "thiserror 2.0.16", "time", ] @@ -11865,7 +12220,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", ] [[package]] @@ -11971,7 +12326,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-stream", "tracing", @@ -12027,7 +12382,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.1", + "bitflags 2.9.2", "byteorder", "bytes", "chrono", @@ -12057,7 +12412,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.14", + "thiserror 2.0.16", "tracing", "uuid", "whoami", @@ -12072,7 +12427,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.1", + "bitflags 2.9.2", "byteorder", "chrono", "crc", @@ -12098,7 +12453,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.14", + "thiserror 2.0.16", "tracing", "uuid", "whoami", @@ -12124,7 +12479,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.14", + "thiserror 2.0.16", "tracing", "url", "uuid", @@ -12386,7 +12741,7 @@ version = "0.118.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6f866d12e4d519052b92a0a86d1ac7ff17570da1272ca0c89b3d6f802cd79df" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "is-macro", "num-bigint", "phf 0.11.3", @@ -12472,7 +12827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65f21494e75d0bd8ef42010b47cabab9caaed8f2207570e809f6f4eb51a710d1" dependencies = [ "better_scoped_tls", - "bitflags 2.9.1", + "bitflags 2.9.2", "indexmap 2.10.0", "once_cell", "phf 0.11.3", @@ -12740,7 +13095,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "byteorder", "enum-as-inner", "libc", @@ -12754,7 +13109,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "byteorder", "enum-as-inner", "libc", @@ -12793,7 +13148,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -12864,7 +13219,7 @@ dependencies = [ "lru 0.12.5", "lz4_flex", "measure_time", - "memmap2 0.9.7", + "memmap2 0.9.8", "once_cell", "oneshot", "rayon", @@ -12883,7 +13238,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.16", "time", "uuid", "winapi", @@ -12995,15 +13350,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -13055,11 +13410,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.16", ] [[package]] @@ -13075,9 +13430,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -13227,9 +13582,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -13595,7 +13950,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -13645,6 +14000,7 @@ dependencies = [ "pin-project-lite", "sync_wrapper 1.0.2", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -13674,7 +14030,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "async-compression", - "bitflags 2.9.1", + "base64 0.22.1", + "bitflags 2.9.2", "bytes", "futures-core", "futures-util", @@ -13682,6 +14039,7 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "iri-string", + "mime", "pin-project-lite", "tokio", "tokio-util", @@ -13991,6 +14349,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "ug" version = "0.4.0" @@ -14000,7 +14364,7 @@ dependencies = [ "gemm 0.18.2", "half", "libloading 0.8.8", - "memmap2 0.9.7", + "memmap2 0.9.8", "num", "num-traits", "num_cpus", @@ -14209,6 +14573,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "unscanny" version = "0.1.0" @@ -14249,9 +14619,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" dependencies = [ "form_urlencoded", "idna", @@ -14326,7 +14696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a511192602f7b435b0a241c1947aa743eb7717f20a9195f4b5e8ed1952e01db1" dependencies = [ "bindgen 0.70.1", - "bitflags 2.9.1", + "bitflags 2.9.2", "fslock", "gzip-header", "home", @@ -14342,7 +14712,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97599c400fc79925922b58303e98fcb8fa88f573379a08ddb652e72cbd2e70f6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "encoding_rs", "indexmap 2.10.0", "num-bigint", @@ -14566,7 +14936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eeee3bdea6257cc36d756fa745a70f9d393571e47d69e0ed97581676a5369ca" dependencies = [ "deno_error", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -14633,7 +15003,7 @@ checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" dependencies = [ "arrayvec", "bit-vec 0.6.3", - "bitflags 2.9.1", + "bitflags 2.9.2", "cfg_aliases 0.1.1", "codespan-reporting", "document-features", @@ -14664,7 +15034,7 @@ dependencies = [ "arrayvec", "ash", "bit-set 0.5.3", - "bitflags 2.9.1", + "bitflags 2.9.2", "block", "cfg_aliases 0.1.1", "core-graphics-types", @@ -14702,7 +15072,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", "js-sys", "serde", "web-sys", @@ -14767,11 +15137,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -14782,7 +15152,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "aws-sdk-config", @@ -14796,6 +15166,8 @@ dependencies = [ "futures", "gethostname", "git-version", + "k8s-openapi", + "kube", "lazy_static", "memchr", "object_store", @@ -14836,7 +15208,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "argon2", @@ -14878,7 +15250,7 @@ dependencies = [ "hf-hub", "hmac", "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "indexmap 2.10.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -14918,7 +15290,7 @@ dependencies = [ "sql-builder", "sqlx", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.16", "time", "tinyvector", "tokenizers", @@ -14941,6 +15313,7 @@ dependencies = [ "urlencoding", "uuid", "windmill-audit", + "windmill-autoscaling", "windmill-common", "windmill-git-sync", "windmill-indexer", @@ -14954,7 +15327,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.527.1" +version = "1.532.0" dependencies = [ "base64 0.22.1", "chrono", @@ -14969,7 +15342,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.527.1" +version = "1.532.0" dependencies = [ "chrono", "serde", @@ -14982,12 +15355,17 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", + "axum", + "k8s-openapi", + "kube", "serde", "serde_json", "sqlx", + "thiserror 2.0.16", + "tokio", "tracing", "uuid", "windmill-common", @@ -14996,7 +15374,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", @@ -15021,7 +15399,7 @@ dependencies = [ "git-version", "hex", "hmac", - "hyper 1.6.0", + "hyper 1.7.0", "indexmap 2.10.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -15055,7 +15433,7 @@ dependencies = [ "systemstat", "tar", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.16", "tikv-jemalloc-ctl", "tokio", "tokio-stream", @@ -15077,7 +15455,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.527.1" +version = "1.532.0" dependencies = [ "regex", "serde", @@ -15092,7 +15470,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "bytes", @@ -15116,7 +15494,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.527.1" +version = "1.532.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15128,7 +15506,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.527.1" +version = "1.532.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -15137,7 +15515,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15149,7 +15527,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "serde_json", @@ -15161,7 +15539,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "gosyn", @@ -15173,7 +15551,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15185,7 +15563,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "serde_json", @@ -15197,7 +15575,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "nu-parser", @@ -15208,7 +15586,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15219,7 +15597,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15231,7 +15609,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", @@ -15254,7 +15632,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15268,7 +15646,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15285,7 +15663,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15299,7 +15677,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15317,7 +15695,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "getrandom 0.2.16", @@ -15342,7 +15720,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "serde_json", @@ -15352,7 +15730,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", @@ -15385,7 +15763,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.527.1" +version = "1.532.0" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15395,7 +15773,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.527.1" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", @@ -15462,6 +15840,7 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", + "ulid", "url", "urlencoding", "uuid", @@ -15979,7 +16358,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.2", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 435345eb59..a43cd6716f 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.527.1" +version = "1.532.0" authors.workspace = true edition.workspace = true @@ -33,7 +33,7 @@ members = [ ] [workspace.package] -version = "1.527.1" +version = "1.532.0" authors = ["Ruben Fiszel "] edition = "2021" @@ -49,7 +49,7 @@ incremental = true lto = "thin" [features] -default = ["ruby"] +default = [] private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"] agent_worker_server = ["windmill-api/agent_worker_server"] enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"] @@ -148,6 +148,8 @@ size.workspace = true strum.workspace = true aws-sigv4.workspace = true aws-sdk-config.workspace = true +kube.workspace = true +k8s-openapi.workspace = true [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = { optional = true, workspace = true } @@ -399,6 +401,8 @@ systemstat = "0.2.4" size = "0.5.0" flume = { version = "0.11.1", features = ["async"] } +kube = { version = "1.1.0", features = ["runtime", "derive"] } +k8s-openapi = { version = "0.25.0", features = ["latest"] } # Macro-related proc-macro2 = "1.0" @@ -417,4 +421,3 @@ oracle = { version = "0.6.3", features = ["chrono"] } rumqttc = { version = "0.24.0", features = ["use-native-tls"]} strum = { version = "0.27", features = ["derive"] } strum_macros = "^0" - diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 41a59cdb81..7e96e8cdec 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -d04959121c6869d764e11e005676003fd2919629 \ No newline at end of file +15a7592ca66b93b9760d49e58b23c090ead06fe2 diff --git a/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py b/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py index e82683eb52..797216c364 100644 --- a/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py +++ b/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py @@ -41,7 +41,7 @@ def load_openapi_spec(file_path: str) -> Dict[str, Any]: print(f"Error loading OpenAPI spec: {e}", file=sys.stderr) sys.exit(1) -def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any]) -> tuple: +def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None) -> tuple: """Extract separate schemas for path parameters, query parameters, and request body.""" path_params_schema = { "type": "object", @@ -92,6 +92,20 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt # Process request body if present if request_body: body_schema = extract_request_body_schema(request_body, spec) + + # If we have required fields specified and a body schema, update the required array + if body_schema and required_fields: + if 'required' not in body_schema: + body_schema['required'] = [] + + # Add each required field if it exists in the schema properties + for field in required_fields: + if 'properties' in body_schema and field in body_schema['properties']: + if field not in body_schema['required']: + body_schema['required'].append(field) + else: + # Log warning when a required field is missing from schema properties + print(f"Warning: Required field '{field}' not found in body schema properties", file=sys.stderr) # Return None for empty schemas path_params_schema = path_params_schema if path_params_schema['properties'] else None @@ -199,6 +213,7 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]: 'method': method.upper(), 'parameters': operation.get('parameters', []), 'requestBody': operation.get('requestBody'), + 'required_fields': operation.get('x-mcp-required-fields', []), } tools.append(tool) @@ -227,7 +242,7 @@ pub fn all_tools() -> Vec { # Generate separate schemas path_params_schema, query_params_schema, body_schema = extract_separate_schemas( - tool['parameters'], tool['requestBody'], spec + tool['parameters'], tool['requestBody'], spec, tool['required_fields'] ) path_params_rust = schema_to_rust_value(path_params_schema) diff --git a/backend/migrations/20250814114112_add_aiagent_job_kind.down.sql b/backend/migrations/20250814114112_add_aiagent_job_kind.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20250814114112_add_aiagent_job_kind.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20250814114112_add_aiagent_job_kind.up.sql b/backend/migrations/20250814114112_add_aiagent_job_kind.up.sql new file mode 100644 index 0000000000..3427949748 --- /dev/null +++ b/backend/migrations/20250814114112_add_aiagent_job_kind.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TYPE JOB_KIND ADD VALUE IF NOT EXISTS 'aiagent'; \ No newline at end of file diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 31f376d4b6..27a56d5613 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -114,6 +114,12 @@ lazy_static::lazy_static! { &["tag"] ).unwrap(); + static ref QUEUE_RUNNING_COUNT: prometheus::IntGaugeVec = prometheus::register_int_gauge_vec!( + "queue_running_count", + "Number of running jobs in the queue", + &["tag"] + ).unwrap(); + } lazy_static::lazy_static! { static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT") @@ -140,6 +146,7 @@ lazy_static::lazy_static! { pub static ref WORKERS_NAMES: Arc>> = Arc::new(RwLock::new(Vec::new())); static ref QUEUE_COUNT_TAGS: Arc>> = Arc::new(RwLock::new(Vec::new())); + static ref QUEUE_RUNNING_COUNT_TAGS: Arc>> = Arc::new(RwLock::new(Vec::new())); static ref DISABLE_CONCURRENCY_LIMIT: bool = std::env::var("DISABLE_CONCURRENCY_LIMIT").is_ok_and(|s| s == "true"); static ref STALE_JOB_TRESHOLD_MINUTES: Option = std::env::var("STALE_JOB_TRESHOLD_MINUTES") @@ -1324,7 +1331,10 @@ pub async fn reload_url_list_setting( match url::Url::parse(url_str) { Ok(url) => urls.push(url), Err(e) => { - return Err(error::Error::BadRequest(format!("Invalid URL in FORCE_{}: '{}': {}", std_env_var, url_str, e))); + return Err(error::Error::BadRequest(format!( + "Invalid URL in FORCE_{}: '{}': {}", + std_env_var, url_str, e + ))); } } } @@ -1347,7 +1357,11 @@ pub async fn reload_url_list_setting( } } } - if urls.is_empty() { None } else { Some(urls) } + if urls.is_empty() { + None + } else { + Some(urls) + } } else { None }; @@ -1365,7 +1379,11 @@ pub async fn reload_url_list_setting( } } } - tracing::info!("Loaded setting {} from db config: {} URLs", setting_name, urls.len()); + tracing::info!( + "Loaded setting {} from db config: {} URLs", + setting_name, + urls.len() + ); value = if urls.is_empty() { None } else { Some(urls) }; } else { tracing::error!("Could not parse {} found: {:#?}", setting_name, &q); @@ -1671,6 +1689,30 @@ pub async fn expose_queue_metrics(db: &Pool) { let mut w = QUEUE_COUNT_TAGS.write().await; *w = tags_to_watch; } + + #[cfg(feature = "prometheus")] + if metrics_enabled { + // Handle queue running count metrics + let queue_running_counts = windmill_common::queue::get_queue_running_counts(db).await; + + for q in QUEUE_RUNNING_COUNT_TAGS.read().await.iter() { + if queue_running_counts.get(q).is_none() { + (*QUEUE_RUNNING_COUNT).with_label_values(&[q]).set(0); + } + } + + let mut running_tags_to_watch = vec![]; + for q in queue_running_counts { + let count = q.1; + let tag = q.0; + + let metric = (*QUEUE_RUNNING_COUNT).with_label_values(&[&tag]); + metric.set(count as i64); + running_tags_to_watch.push(tag.to_string()); + } + let mut w = QUEUE_RUNNING_COUNT_TAGS.write().await; + *w = running_tags_to_watch; + } } // clean queue metrics older than 14 days diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index a8631f3484..b58c74fbee 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -959,6 +959,7 @@ impl RunJob { None, None, None, + false, ) .await .expect("push has to succeed"); diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index d5a9aa8266..551f1689b8 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -50,6 +50,7 @@ windmill-parser-py.workspace = true windmill-parser-py-imports.workspace = true windmill-git-sync.workspace = true windmill-indexer = { workspace = true, optional = true } +windmill-autoscaling.workspace = true windmill-worker.workspace = true tokio.workspace = true tokio-stream.workspace = true diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1c1af96d05..cdd3f70554 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.527.1 + version: 1.532.0 title: Windmill API contact: @@ -7193,6 +7193,36 @@ paths: type: string format: uuid + /w/{workspace}/jobs/run_wait_result/preview: + post: + summary: run script preview and wait for result + operationId: runScriptPreviewAndWaitResult + x-mcp-tool: true + x-mcp-instructions: Allows testing a script before deploying it. For typescript code, the language to send is either bun or deno. By default, send bun if no deno specific code is detected. + x-mcp-required-fields: + - content + - language + - args + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + + requestBody: + description: preview + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Preview" + + responses: + "200": + description: job result + content: + application/json: + schema: {} + /w/{workspace}/jobs/workflow_as_code/{job_id}/{entrypoint}: post: summary: run code-workflow task @@ -7302,6 +7332,30 @@ paths: type: string format: uuid + /w/{workspace}/jobs/run_wait_result/preview_flow: + post: + summary: run flow preview and wait for result + operationId: runFlowPreviewAndWaitResult + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + + requestBody: + description: preview + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/FlowPreview" + + responses: + "200": + description: job result + content: + application/json: + schema: {} + /w/{workspace}/jobs/queue/list: get: summary: list all queued jobs @@ -11502,6 +11556,22 @@ paths: additionalProperties: type: integer + /workers/queue_running_counts: + get: + summary: get counts of currently running jobs per tag + operationId: getCountsOfRunningJobsPerTag + tags: + - worker + responses: + "200": + description: queue running counts + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + /configs/list_worker_groups: get: summary: list worker groups @@ -11615,6 +11685,22 @@ paths: items: $ref: "#/components/schemas/AutoscalingEvent" + /configs/native_kubernetes_autoscaling_healthcheck: + get: + summary: Check Kubernetes autoscaling health for a worker group + operationId: nativeKubernetesAutoscalingHealthcheck + tags: + - config + responses: + "200": + description: Kubernetes autoscaling is healthy + "400": + description: Error + content: + text/plain: + schema: + type: string + /configs/list_available_python_versions: get: summary: Get currently available python versions provided by UV. @@ -13929,6 +14015,8 @@ components: $ref: "../../openflow.openapi.yaml#/components/schemas/BranchOne" BranchAll: $ref: "../../openflow.openapi.yaml#/components/schemas/BranchAll" + AiAgent: + $ref: "../../openflow.openapi.yaml#/components/schemas/AiAgent" Identity: $ref: "../../openflow.openapi.yaml#/components/schemas/Identity" FlowStatus: @@ -14411,6 +14499,7 @@ components: "flowscript", "flownode", "appscript", + "aiagent", ] schedule_path: type: string @@ -14520,6 +14609,7 @@ components: "flowscript", "flownode", "appscript", + "aiagent", ] schedule_path: type: string @@ -15138,10 +15228,13 @@ components: properties: content: type: string + description: The code to run path: type: string + description: The path to the script script_hash: type: string + description: The hash of the script args: $ref: "#/components/schemas/ScriptArgs" language: diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index dc128997ee..37743eae34 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -762,20 +762,26 @@ async fn get_public_resource( Path((w_id, path)): Path<(String, StripPath)>, ) -> JsonResult> { let path = path.to_path(); - if !path.starts_with("f/app_themes/") { - return Err(Error::BadRequest( - "Only app themes are public resources".to_string(), - )); - } - let res = sqlx::query_scalar!( - "SELECT value from resource WHERE path = $1 AND workspace_id = $2", - path.to_owned(), - &w_id - ) - .fetch_optional(&db) - .await? - .flatten(); - Ok(Json(res)) + + let res = if path.starts_with("f/app_themes/") { + sqlx::query_scalar!( + "SELECT value from resource WHERE path = $1 AND workspace_id = $2", + path.to_owned(), + &w_id + ) + .fetch_optional(&db) + .await? + } else { + sqlx::query_scalar!( + "SELECT value from resource WHERE path = $1 AND workspace_id = $2 AND resource_type = 'json_schema'", + path.to_owned(), + &w_id + ) + .fetch_optional(&db) + .await? + }; + + Ok(Json(res.flatten())) } async fn get_secret_id( @@ -1092,6 +1098,7 @@ async fn create_app_internal<'a>( None, None, Some(&authed.clone().into()), + false, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1469,6 +1476,7 @@ async fn update_app_internal<'a>( None, None, Some(&authed.clone().into()), + false, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1782,6 +1790,7 @@ async fn execute_component( None, None, None, + false, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/configs.rs b/backend/windmill-api/src/configs.rs index b53ae414b5..3d7c47f89e 100644 --- a/backend/windmill-api/src/configs.rs +++ b/backend/windmill-api/src/configs.rs @@ -34,6 +34,10 @@ pub fn global_service() -> Router { "/list_autoscaling_events/:worker_group", get(list_autoscaling_events), ) + .route( + "/native_kubernetes_autoscaling_healthcheck", + get(native_kubernetes_autoscaling_healthcheck), + ) .route( "/list_available_python_versions", get(list_available_python_versions), @@ -246,6 +250,26 @@ async fn list_autoscaling_events( Ok(Json(events)) } +#[cfg(all(feature = "enterprise", feature = "private"))] +async fn native_kubernetes_autoscaling_healthcheck( + authed: ApiAuthed, + Extension(db): Extension, +) -> Result<(), windmill_autoscaling::kubernetes_integration_ee::KubeError> { + require_devops_role(&db, &authed.email).await.map_err(|e| { + windmill_autoscaling::kubernetes_integration_ee::KubeError::Other(e.to_string()) + })?; + + windmill_autoscaling::kubernetes_integration_ee::kubernetes_healthcheck().await +} + +#[cfg(not(all(feature = "enterprise", feature = "private")))] +async fn native_kubernetes_autoscaling_healthcheck( +) -> Result<(), error::Error> { + Err(error::Error::BadRequest( + "Native Kubernetes autoscaling available only in the enterprise version".to_string(), + )) +} + async fn list_available_python_versions() -> error::JsonResult> { #[cfg(not(feature = "python"))] return Err(error::Error::BadRequest( diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index a54c3655f5..9ea840a2d1 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -519,6 +519,7 @@ async fn create_flow( None, None, Some(&authed.clone().into()), + false, ) .await?; @@ -985,6 +986,7 @@ async fn update_flow( None, None, Some(&authed.clone().into()), + false, ) .await?; sqlx::query!( diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 2e1296c7e1..c29e4e6f02 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -177,12 +177,14 @@ pub fn workspaced_service() -> Router { .layer(ce_headers.clone()), ) .route("/run/preview", post(run_preview_script)) + .route("/run_wait_result/preview", post(run_wait_result_preview_script)) .route( "/run/preview_bundle", post(run_bundle_preview_script).layer(axum::extract::DefaultBodyLimit::disable()), ) .route("/add_batch_jobs/:n", post(add_batch_jobs)) .route("/run/preview_flow", post(run_preview_flow_job)) + .route("/run_wait_result/preview_flow", post(run_wait_result_preview_flow)) .route("/list", get(list_jobs)) .route( "/list_selected_job_groups", @@ -3883,6 +3885,7 @@ pub async fn run_flow_by_path_inner( None, None, push_authed.as_ref(), + false ) .await?; tx.commit().await?; @@ -3977,6 +3980,7 @@ pub async fn restart_flow( None, completed_job.priority, Some(&authed.clone().into()), + false ) .await?; tx.commit().await?; @@ -4072,6 +4076,7 @@ pub async fn run_script_by_path_inner( None, None, push_authed.as_ref(), + false, ) .await?; tx.commit().await?; @@ -4220,6 +4225,7 @@ pub async fn run_workflow_as_code( None, None, push_authed.as_ref(), + false, ) .await?; @@ -4750,6 +4756,7 @@ pub async fn run_wait_result_job_by_path_get( None, None, push_authed.as_ref(), + false, ) .await?; tx.commit().await?; @@ -4900,6 +4907,7 @@ pub async fn run_wait_result_script_by_path_internal( None, None, push_authed.as_ref(), + false, ) .await?; tx.commit().await?; @@ -5014,6 +5022,7 @@ pub async fn run_wait_result_script_by_hash( None, None, push_authed.as_ref(), + false, ) .await?; tx.commit().await?; @@ -5131,6 +5140,7 @@ pub async fn run_wait_result_flow_by_path_internal( None, None, push_authed.as_ref(), + false, ) .await?; tx.commit().await?; @@ -5200,6 +5210,7 @@ async fn run_preview_script( None, None, Some(&authed.clone().into()), + false, ) .await?; tx.commit().await?; @@ -5207,6 +5218,28 @@ async fn run_preview_script( Ok((StatusCode::CREATED, uuid.to_string())) } +async fn run_wait_result_preview_script( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path(w_id): Path, + Query(run_query): Query, + Json(preview): Json, +) -> error::Result { + + let (_status_code, uuid) = run_preview_script( + authed.clone(), + Extension(db.clone()), + Extension(user_db.clone()), + Path(w_id.clone()), + Query(run_query.clone()), + Json(preview) + ).await?; + let uuid = uuid.parse::().map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?; + let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + return result; +} + async fn run_bundle_preview_script( authed: ApiAuthed, Extension(db): Extension, @@ -5288,6 +5321,7 @@ async fn run_bundle_preview_script( None, None, Some(&authed.clone().into()), + false, ) .await?; job_id = Some(uuid); @@ -5453,6 +5487,7 @@ async fn run_dependencies_job( None, None, Some(&authed.clone().into()), + false, ) .await?; tx.commit().await?; @@ -5518,6 +5553,7 @@ async fn run_flow_dependencies_job( None, None, Some(&authed.clone().into()), + false, ) .await?; tx.commit().await?; @@ -5858,6 +5894,7 @@ async fn run_preview_flow_job( None, None, Some(&authed.clone().into()), + false, ) .await?; tx.commit().await?; @@ -5865,6 +5902,20 @@ async fn run_preview_flow_job( Ok((StatusCode::CREATED, uuid.to_string())) } +async fn run_wait_result_preview_flow( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path(w_id): Path, + Query(run_query): Query, + Json(raw_flow): Json, +) -> error::Result { + let (_status_code, uuid) = run_preview_flow_job(authed.clone(), Extension(db.clone()), Extension(user_db.clone()), Path(w_id.clone()), Query(run_query.clone()), Json(raw_flow)).await?; + let uuid = uuid.parse::().map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?; + let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + return result; +} + pub async fn run_job_by_hash( authed: ApiAuthed, Extension(db): Extension, @@ -5983,6 +6034,7 @@ pub async fn run_job_by_hash_inner( None, None, push_authed.as_ref(), + false, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs b/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs index 9865da06c9..f2874fff47 100644 --- a/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs +++ b/backend/windmill-api/src/mcp/tools/auto_generated_endpoints.rs @@ -564,6 +564,86 @@ pub fn all_tools() -> Vec { })), body_schema: None, }, + EndpointTool { + name: Cow::Borrowed("runScriptPreviewAndWaitResult"), + description: Cow::Borrowed("run script preview and wait for result"), + instructions: Cow::Borrowed("Allows testing a script before deploying it. For typescript code, the language to send is either bun or deno. By default, send bun if no deno specific code is detected."), + path: Cow::Borrowed("/w/{workspace}/jobs/run_wait_result/preview"), + method: Cow::Borrowed("POST"), + path_params_schema: None, + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The code to run" + }, + "path": { + "type": "string", + "description": "The path to the script" + }, + "script_hash": { + "type": "string", + "description": "The hash of the script" + }, + "args": { + "type": "object", + "description": "The arguments to pass to the script or flow", + "additionalProperties": {} + }, + "language": { + "type": "string", + "enum": [ + "python3", + "deno", + "go", + "bash", + "powershell", + "postgresql", + "mysql", + "bigquery", + "snowflake", + "mssql", + "oracledb", + "graphql", + "nativets", + "bun", + "php", + "rust", + "ansible", + "csharp", + "nu", + "java", + "ruby", + "duckdb" + ] + }, + "tag": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "code", + "identity", + "http" + ] + }, + "dedicated_worker": { + "type": "boolean" + }, + "lock": { + "type": "string" + } + }, + "required": [ + "args", + "content", + "language" + ] +})), + }, EndpointTool { name: Cow::Borrowed("listQueue"), description: Cow::Borrowed("list all queued jobs"), diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index ca2cec7ce1..de2533a954 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -1023,6 +1023,7 @@ async fn create_script_internal<'c>( None, None, Some(&authed.clone().into()), + false, ) .await?; Ok((hash, new_tx, None)) diff --git a/backend/windmill-api/src/trigger_helpers.rs b/backend/windmill-api/src/trigger_helpers.rs index f20496c1d0..723a966bca 100644 --- a/backend/windmill-api/src/trigger_helpers.rs +++ b/backend/windmill-api/src/trigger_helpers.rs @@ -798,6 +798,7 @@ async fn trigger_script_with_retry_and_error_handler( None, None, push_authed.as_ref(), + false, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/workers.rs b/backend/windmill-api/src/workers.rs index 8e6b7a86fd..1165a5e2f3 100644 --- a/backend/windmill-api/src/workers.rs +++ b/backend/windmill-api/src/workers.rs @@ -37,6 +37,7 @@ pub fn global_service() -> Router { .route("/get_default_tags", get(get_default_tags)) .route("/queue_metrics", get(get_queue_metrics)) .route("/queue_counts", get(get_queue_counts)) + .route("/queue_running_counts", get(get_queue_running_counts)) } #[derive(FromRow, Serialize, Deserialize)] @@ -219,3 +220,12 @@ async fn get_queue_counts( let queue_counts = windmill_common::queue::get_queue_counts(&db).await; Ok(Json(queue_counts)) } + +async fn get_queue_running_counts( + authed: ApiAuthed, + Extension(db): Extension, +) -> JsonResult> { + require_super_admin(&db, &authed.email).await?; + let queue_running_counts = windmill_common::queue::get_queue_running_counts(&db).await; + Ok(Json(queue_running_counts)) +} diff --git a/backend/windmill-autoscaling/Cargo.toml b/backend/windmill-autoscaling/Cargo.toml index 7e4bc623ad..fd5c5f4703 100644 --- a/backend/windmill-autoscaling/Cargo.toml +++ b/backend/windmill-autoscaling/Cargo.toml @@ -21,4 +21,9 @@ serde_json.workspace = true tracing.workspace = true windmill-common = { workspace = true, default-features = false } windmill-queue.workspace = true -anyhow.workspace = true \ No newline at end of file +anyhow.workspace = true +kube.workspace = true +k8s-openapi.workspace = true +tokio.workspace = true +thiserror.workspace = true +axum.workspace = true diff --git a/backend/windmill-autoscaling/src/lib.rs b/backend/windmill-autoscaling/src/lib.rs index 1b9d6b94bf..1169b51977 100644 --- a/backend/windmill-autoscaling/src/lib.rs +++ b/backend/windmill-autoscaling/src/lib.rs @@ -2,3 +2,7 @@ pub mod autoscaling_ee; mod autoscaling_oss; pub use autoscaling_oss::*; +#[cfg(feature = "private")] +pub mod kubernetes_integration_ee; +#[cfg(feature = "private")] +pub use kubernetes_integration_ee::{apply_kubernetes_autoscaling, KubernetesIntegration}; diff --git a/backend/windmill-common/src/auth.rs b/backend/windmill-common/src/auth.rs index 543d6eb462..97aa27e9d8 100644 --- a/backend/windmill-common/src/auth.rs +++ b/backend/windmill-common/src/auth.rs @@ -8,6 +8,7 @@ use crate::{ error::{Error, Result}, jwt, users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL}, + utils::WarnAfterExt, DB, }; @@ -259,6 +260,22 @@ pub async fn get_groups_for_user( Ok(groups) } +pub async fn get_job_perms<'a, E: sqlx::PgExecutor<'a>>( + db: E, + job_id: &Uuid, + w_id: &str, +) -> sqlx::Result> { + sqlx::query_as!( + JobPerms, + "SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2", + job_id, + w_id + ) + .fetch_optional(db) + .warn_after_seconds(3) + .await +} + #[tracing::instrument(level = "trace", skip_all)] pub async fn create_token_for_owner( db: &DB, @@ -274,14 +291,7 @@ pub async fn create_token_for_owner( let job_perms = if perms.is_some() { Ok(perms) } else { - sqlx::query_as!( - JobPerms, - "SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2", - job_id, - w_id - ) - .fetch_optional(db) - .await + get_job_perms(db, job_id, w_id).await }; let job_authed = match job_perms { Ok(Some(jp)) => jp.into(), @@ -297,8 +307,16 @@ pub async fn create_token_for_owner( } }; - create_jwt_token(job_authed, w_id, expires_in, Some(*job_id), Some(label.to_string()), audit_span, None) - .await + create_jwt_token( + job_authed, + w_id, + expires_in, + Some(*job_id), + Some(label.to_string()), + audit_span, + None, + ) + .await } pub async fn create_jwt_token( @@ -319,8 +337,8 @@ pub async fn create_jwt_token( folders: authed.folders.clone(), label, workspace_id: workspace_id.to_string(), - exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64)) - .timestamp() as usize, + exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64)).timestamp() + as usize, job_id: job_id.map(|id| id.to_string()), scopes, audit_span, diff --git a/backend/windmill-common/src/flow_status.rs b/backend/windmill-common/src/flow_status.rs index 50c7cf0e61..1ae95843b7 100644 --- a/backend/windmill-common/src/flow_status.rs +++ b/backend/windmill-common/src/flow_status.rs @@ -129,6 +129,15 @@ struct UntaggedFlowStatusModule { approvers: Option>, failed_retries: Option>, skipped: Option, + agent_actions: Option>, + agent_actions_success: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AgentAction { + ToolCall { job_id: uuid::Uuid, function_name: String, module_id: String }, + Message {}, } #[derive(Serialize, Debug, Clone)] @@ -165,6 +174,10 @@ pub enum FlowStatusModule { parallel: bool, #[serde(skip_serializing_if = "std::ops::Not::not")] while_loop: bool, + #[serde(skip_serializing_if = "Option::is_none")] + agent_actions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + agent_actions_success: Option>, }, Success { id: String, @@ -181,6 +194,10 @@ pub enum FlowStatusModule { #[serde(skip_serializing_if = "Vec::is_empty")] failed_retries: Vec, skipped: bool, + #[serde(skip_serializing_if = "Option::is_none")] + agent_actions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + agent_actions_success: Option>, }, Failure { id: String, @@ -193,6 +210,10 @@ pub enum FlowStatusModule { branch_chosen: Option, #[serde(skip_serializing_if = "Vec::is_empty")] failed_retries: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + agent_actions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + agent_actions_success: Option>, }, } @@ -244,6 +265,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule { parallel: untagged.parallel.unwrap_or(false), while_loop: untagged.while_loop.unwrap_or(false), progress: untagged.progress, + agent_actions: untagged.agent_actions, + agent_actions_success: untagged.agent_actions_success, }), "Success" => Ok(FlowStatusModule::Success { id: untagged @@ -258,6 +281,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule { approvers: untagged.approvers.unwrap_or_default(), failed_retries: untagged.failed_retries.unwrap_or_default(), skipped: untagged.skipped.unwrap_or(false), + agent_actions: untagged.agent_actions, + agent_actions_success: untagged.agent_actions_success, }), "Failure" => Ok(FlowStatusModule::Failure { id: untagged @@ -270,6 +295,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule { flow_jobs_success: untagged.flow_jobs_success, branch_chosen: untagged.branch_chosen, failed_retries: untagged.failed_retries.unwrap_or_default(), + agent_actions: untagged.agent_actions, + agent_actions_success: untagged.agent_actions_success, }), other => Err(serde::de::Error::unknown_variant( other, @@ -354,6 +381,30 @@ impl FlowStatusModule { _ => false, } } + + pub fn agent_actions(&self) -> Option> { + match self { + FlowStatusModule::InProgress { agent_actions, .. } => agent_actions.clone(), + FlowStatusModule::Success { agent_actions, .. } => agent_actions.clone(), + FlowStatusModule::Failure { agent_actions, .. } => agent_actions.clone(), + _ => None, + } + } + + pub fn agent_actions_success(&self) -> Option> { + match self { + FlowStatusModule::InProgress { agent_actions_success, .. } => { + agent_actions_success.clone() + } + FlowStatusModule::Success { agent_actions_success, .. } => { + agent_actions_success.clone() + } + FlowStatusModule::Failure { agent_actions_success, .. } => { + agent_actions_success.clone() + } + _ => None, + } + } } impl FlowStatus { diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index 52146213d0..60583ef50f 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -529,6 +529,10 @@ pub enum FlowModuleValue { #[serde(skip_serializing_if = "Option::is_none")] assets: Option>, }, + AIAgent { + input_transforms: HashMap, + tools: Vec, + }, } fn is_none_or_empty(expr: &Option) -> bool { @@ -563,6 +567,7 @@ struct UntaggedFlowModuleValue { default_node: Option, modules_node: Option, assets: Option>, + tools: Option>, } impl<'de> Deserialize<'de> for FlowModuleValue { @@ -655,6 +660,12 @@ impl<'de> Deserialize<'de> for FlowModuleValue { assets: untagged.assets, }), "identity" => Ok(FlowModuleValue::Identity), + "aiagent" => Ok(FlowModuleValue::AIAgent { + input_transforms: untagged.input_transforms.unwrap_or_default(), + tools: untagged + .tools + .ok_or_else(|| serde::de::Error::missing_field("tools"))?, + }), other => Err(serde::de::Error::unknown_variant( other, &[ @@ -666,6 +677,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue { "branchall", "rawscript", "identity", + "aiagent", ], )), } diff --git a/backend/windmill-common/src/jobs.rs b/backend/windmill-common/src/jobs.rs index c2be3abf6d..4b529873a4 100644 --- a/backend/windmill-common/src/jobs.rs +++ b/backend/windmill-common/src/jobs.rs @@ -48,6 +48,7 @@ pub enum JobKind { FlowScript, FlowNode, AppScript, + AIAgent, } impl JobKind { @@ -375,6 +376,9 @@ pub enum JobPayload { }, Identity, Noop, + AIAgent { + path: String, + }, } #[derive(Clone, Serialize, Deserialize, Debug, Default)] diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 4b5408289f..e081cb3ceb 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -627,8 +627,8 @@ pub fn get_latest_flow_version_info_for_path< } } -pub async fn get_latest_hash_for_path<'c>( - db: &mut sqlx::Transaction<'c, sqlx::Postgres>, +pub async fn get_latest_hash_for_path<'c, E: sqlx::PgExecutor<'c>>( + db: E, w_id: &str, script_path: &str, ) -> error::Result<( @@ -652,7 +652,7 @@ pub async fn get_latest_hash_for_path<'c>( script_path, w_id ) - .fetch_optional(&mut **db) + .fetch_optional(db) .await?; let script = utils::not_found_if_none(r_o, "script", script_path)?; diff --git a/backend/windmill-common/src/queue.rs b/backend/windmill-common/src/queue.rs index 39ec349c91..2656ce5af5 100644 --- a/backend/windmill-common/src/queue.rs +++ b/backend/windmill-common/src/queue.rs @@ -14,3 +14,16 @@ pub async fn get_queue_counts(db: &Pool) -> HashMap { .map(|v| v.into_iter().map(|x| (x.tag, x.count as u32)).collect()) .unwrap_or_else(|| HashMap::new()) } + +pub async fn get_queue_running_counts(db: &Pool) -> HashMap { + sqlx::query!( + "SELECT tag AS \"tag!\", count(*) AS \"count!\" FROM v2_job_queue WHERE + running = true + GROUP BY tag", + ) + .fetch_all(db) + .await + .ok() + .map(|v| v.into_iter().map(|x| (x.tag, x.count as u32)).collect()) + .unwrap_or_else(|| HashMap::new()) +} diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 50ed13ff7b..7ec4cd09f5 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -642,6 +642,12 @@ pub struct PythonAnnotations { pub py313: bool, } +#[derive(Copy, Clone)] +#[annotations("//")] +pub struct GoAnnotations { + pub go1_22_compat: bool, +} + #[annotations("//")] pub struct TypeScriptAnnotations { pub npm: bool, diff --git a/backend/windmill-queue/src/flow_status.rs b/backend/windmill-queue/src/flow_status.rs index 3bc6b3c815..165a161e65 100644 --- a/backend/windmill-queue/src/flow_status.rs +++ b/backend/windmill-queue/src/flow_status.rs @@ -117,7 +117,7 @@ pub async fn update_workflow_as_code_status( // TODO: merge as a CTE #[tracing::instrument(level = "trace", skip_all)] -async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result { +pub async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result { let r = sqlx::query!( "SELECT (flow_status->'step')::integer as step, jsonb_array_length(flow_status->'modules') as len FROM v2_job_status WHERE id = $1", diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 1b5a67e99f..09b2a39695 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -448,6 +448,7 @@ pub async fn push_init_job<'c>( None, None, None, + false, ) .await?; inner_tx.commit().await?; @@ -500,6 +501,7 @@ pub async fn push_periodic_bash_job<'c>( None, None, None, + false, ) .await?; inner_tx.commit().await?; @@ -1274,6 +1276,7 @@ async fn restart_job_if_perpetual_inner( None, queued_job.priority, None, + false, ) .await?; tx.commit().await?; @@ -2036,6 +2039,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>( None, priority, None, + false, ) .await?; tx.commit().await?; @@ -2144,6 +2148,7 @@ async fn handle_recovered_schedule<'a, 'c, T: Serialize + Send + Sync>( None, None, None, + false, ) .await?; tracing::info!( @@ -2233,6 +2238,7 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>( None, None, None, + false, ) .await?; tracing::info!( @@ -3618,7 +3624,7 @@ pub async fn push<'c, 'd>( root_job: Option, job_id: Option, _is_flow_step: bool, - mut same_worker: bool, + mut same_worker: bool, // whether the job will be executed on the same worker: if true, the job will be set to running but started_at will not be set. pre_run_error: Option<&windmill_common::error::Error>, visible_to_owner: bool, mut tag: Option, @@ -3626,6 +3632,7 @@ pub async fn push<'c, 'd>( flow_step_id: Option, _priority_override: Option, authed: Option<&Authed>, + running: bool, // whether the job is already running: only set this to true if you don't want the job to be picked up by a worker from the queue. It will also set started_at to now. ) -> Result<(Uuid, Transaction<'c, Postgres>), Error> { #[cfg(feature = "cloud")] if *CLOUD_HOSTED { @@ -4435,6 +4442,21 @@ pub async fn push<'c, 'd>( None, None, ), + JobPayload::AIAgent { path } => ( + None, + Some(path), + None, + JobKind::AIAgent, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ), }; let final_priority: Option; @@ -4462,7 +4484,7 @@ pub async fn push<'c, 'd>( final_priority }; - let is_running = same_worker; + let is_running = same_worker || running; if let Some(flow) = raw_flow.as_ref() { same_worker = same_worker || flow.same_worker; @@ -4508,7 +4530,10 @@ pub async fn push<'c, 'd>( let per_workspace = per_workspace_tag(&workspace_id).await; let default = || { - let ntag = if job_kind.is_flow() || job_kind == JobKind::Identity { + let ntag = if job_kind.is_flow() + || job_kind == JobKind::Identity + || job_kind == JobKind::AIAgent + { "flow".to_string() } else if job_kind == JobKind::Dependencies || job_kind == JobKind::FlowDependencies @@ -4667,7 +4692,7 @@ pub async fn push<'c, 'd>( ) INSERT INTO v2_job_queue (workspace_id, id, running, scheduled_for, started_at, tag, priority) - VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)", + VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)", job_id, workspace_id, raw_code, @@ -4711,6 +4736,7 @@ pub async fn push<'c, 'd>( job_authed.groups.as_slice(), root_job.or(parent_job), trigger_kind as Option, + running, ) .execute(&mut *tx) .warn_after_seconds(1) @@ -4782,6 +4808,7 @@ pub async fn push<'c, 'd>( JobKind::FlowScript => "jobs.run.flow_script", JobKind::FlowNode => "jobs.run.flow_node", JobKind::AppScript => "jobs.run.app_script", + JobKind::AIAgent => "jobs.run.ai_agent", }; let audit_author = if format!("u/{user}") != permissioned_as && user != permissioned_as { @@ -4971,6 +4998,8 @@ async fn restarted_flows_resolution( parallel, while_loop: false, progress: None, + agent_actions: None, + agent_actions_success: None, }); } Ok(FlowModuleValue::ForloopFlow { parallel, .. }) => { @@ -5009,6 +5038,8 @@ async fn restarted_flows_resolution( parallel, while_loop: false, progress: None, + agent_actions: None, + agent_actions_success: None, }); } _ => { diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index 0c31568968..6e8f8be8cb 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -153,7 +153,7 @@ pub async fn push_scheduled_job<'c>( on_behalf_of_email, created_by, ) = windmill_common::get_latest_hash_for_path( - &mut tx, + &mut *tx, &schedule.workspace_id, &schedule.script_path, ) @@ -302,6 +302,7 @@ pub async fn push_scheduled_job<'c>( None, None, push_authed, + false, ) .await?; diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index 3805171024..bbb1b414ed 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -59,6 +59,7 @@ windmill-git-sync.workspace = true flume.workspace = true sqlx.workspace = true uuid.workspace = true +ulid.workspace = true tracing.workspace = true tokio.workspace = true tokio-stream.workspace = true diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs new file mode 100644 index 0000000000..25638cbae9 --- /dev/null +++ b/backend/windmill-worker/src/ai_executor.rs @@ -0,0 +1,1018 @@ +use async_recursion::async_recursion; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use std::{collections::HashMap, sync::Arc}; +#[cfg(feature = "benchmark")] +use windmill_common::bench::BenchmarkIter; +use windmill_common::{ + auth::get_job_perms, + cache, + client::AuthedClient, + error::{self, to_anyhow, Error}, + flow_status::AgentAction, + flows::{FlowModule, FlowModuleValue}, + get_latest_hash_for_path, + jobs::{JobKind, DB}, + scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang}, + utils::{StripPath, HTTP_CLIENT}, + worker::{to_raw_value, Connection}, +}; +use windmill_parser::Typ; +use windmill_queue::{ + flow_status::{get_step_of_flow_status, Step}, + get_mini_pulled_job, push, CanceledBy, JobCompleted, MiniPulledJob, PushArgs, + PushIsolationLevel, +}; + +use crate::{ + common::{build_args_map, error_to_value, OccupancyMetrics}, + create_job_dir, + handle_child::run_future_with_polling_update_job_poller, + handle_queued_job, parse_sig_of_lang, + result_processor::handle_non_flow_job_error, + worker_flow::{raw_script_to_payload, script_to_payload}, + JobCompletedSender, SendResult, SendResultPayload, +}; + +const MAX_AGENT_ITERATIONS: usize = 10; + +lazy_static::lazy_static! { + static ref TOOL_NAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); +} + +#[derive(Deserialize, Serialize, Clone)] +struct OpenAIFunction { + name: String, + arguments: String, +} + +#[derive(Deserialize, Serialize, Clone)] +struct OpenAIToolCall { + id: String, + function: OpenAIFunction, + r#type: String, +} + +#[derive(Deserialize, Serialize, Clone, Default)] +struct OpenAIMessage { + role: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, + #[serde(skip_serializing)] + agent_action: Option, +} + +/// same as OpenAIMessage but with agent_action field included in the serialization +#[derive(Serialize)] +struct Message<'a> { + #[serde(flatten)] + message: &'a OpenAIMessage, + #[serde(skip_serializing_if = "Option::is_none")] + agent_action: Option<&'a AgentAction>, +} + +#[derive(Deserialize)] +struct OpenAIChoice { + message: OpenAIMessage, +} + +#[derive(Deserialize)] +struct OpenAIResponse { + choices: Vec, +} + +#[derive(Serialize)] +struct OpenAIRequest<'a> { + model: &'a str, + messages: &'a Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option<&'a Vec<&'a ToolDef>>, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_completion_tokens: Option, +} + +#[derive(Serialize, Clone)] +struct ToolDefFunction { + name: String, + parameters: Box, +} + +#[derive(Serialize, Clone)] +struct ToolDef { + r#type: String, + function: ToolDefFunction, +} + +struct Tool { + module: FlowModule, + def: ToolDef, +} + +#[derive(Deserialize, Debug)] +struct AIAgentArgs { + provider: Provider, + system_prompt: String, + user_message: String, + temperature: Option, + max_completion_tokens: Option, +} + +#[derive(Deserialize, Debug)] +struct ProviderResource { + #[serde(alias = "apiKey")] + api_key: String, +} + +#[derive(Deserialize, Debug)] +#[serde(tag = "kind")] +enum Provider { + OpenAI { resource: ProviderResource, model: String }, + Anthropic { resource: ProviderResource, model: String }, +} + +impl Provider { + fn get_api_key(&self) -> &str { + match self { + Provider::OpenAI { resource, .. } => &resource.api_key, + Provider::Anthropic { resource, .. } => &resource.api_key, + } + } + + fn get_model(&self) -> &str { + match self { + Provider::OpenAI { model, .. } => model, + Provider::Anthropic { model, .. } => model, + } + } + + fn get_base_url(&self) -> &str { + match self { + Provider::OpenAI { .. } => "https://api.openai.com/v1", + Provider::Anthropic { .. } => "https://api.anthropic.com/v1", + } + } +} + +#[derive(Serialize)] +struct AIAgentResult<'a> { + output: String, + messages: Vec>, +} + +#[derive(Serialize, Default, Clone, Debug)] +struct OpenAPISchema { + r#type: String, + #[serde(skip_serializing_if = "Option::is_none")] + items: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + properties: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + required: Option>, + #[serde(skip_serializing_if = "Option::is_none", rename = "oneOf")] + one_of: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + r#enum: Option>, +} + +impl OpenAPISchema { + fn from_str(typ: &str) -> Self { + OpenAPISchema { r#type: typ.to_string(), ..Default::default() } + } + + fn from_str_with_enum(typ: &str, enu: &Option>) -> Self { + OpenAPISchema { r#type: typ.to_string(), r#enum: enu.clone(), ..Default::default() } + } + + fn datetime() -> Self { + Self { + r#type: "string".to_string(), + format: Some("date-time".to_string()), + ..Default::default() + } + } + + fn from_typ(typ: &Typ) -> Self { + match typ { + Typ::Str(enu) => Self::from_str_with_enum("string", enu), + Typ::Int => Self::from_str("integer"), + Typ::Float => Self::from_str("number"), + Typ::Bool => Self::from_str("boolean"), + Typ::Bytes => Self::from_str("string"), + Typ::Datetime => Self::datetime(), + Typ::Resource(_) => Self::from_str("string"), + Typ::Email => Self::from_str("string"), + Typ::Sql => Self::from_str("string"), + Typ::DynSelect(_) => Self::from_str("string"), + Typ::List(typ) => OpenAPISchema { + r#type: "array".to_string(), + items: Some(Box::new(Self::from_typ(typ))), + ..Default::default() + }, + Typ::Object(typ) => OpenAPISchema { + r#type: "object".to_string(), + items: None, + properties: typ.props.as_ref().map(|props| { + props + .iter() + .map(|prop| (prop.key.clone(), Box::new(Self::from_typ(&prop.typ)))) + .collect() + }), + required: typ + .props + .as_ref() + .map(|props| props.iter().map(|prop| prop.key.clone()).collect()), + ..Default::default() + }, + Typ::OneOf(variants) => OpenAPISchema { + r#type: "object".to_string(), + one_of: Some( + variants + .iter() + .map(|variant| { + let schema = OpenAPISchema { + r#type: "object".to_string(), + properties: Some( + variant + .properties + .iter() + .map(|prop| { + ( + prop.key.clone(), + Box::new( + if prop.key == "label" || prop.key == "kind" { + Self::from_str_with_enum( + "string", + &Some(vec![variant.label.clone()]), + ) + } else { + Self::from_typ(&prop.typ) + }, + ), + ) + }) + .collect(), + ), + required: Some( + variant + .properties + .iter() + .map(|prop| prop.key.clone()) + .collect(), + ), + ..Default::default() + }; + Box::new(schema) + }) + .collect(), + ), + ..Default::default() + }, + Typ::Unknown => Self::from_str("object"), + } + } +} + +async fn update_flow_status_module_with_actions( + db: &DB, + parent_job: &uuid::Uuid, + actions: &[AgentAction], +) -> Result<(), Error> { + let step = get_step_of_flow_status(db, parent_job.to_owned()).await?; + match step { + Step::Step(step) => { + sqlx::query!( + r#" + UPDATE v2_job_status SET + flow_status = jsonb_set( + flow_status, + array['modules', $3::TEXT, 'agent_actions'], + $2 + ) + WHERE id = $1 + "#, + parent_job, + sqlx::types::Json(actions) as _, + step as i32 + ) + .execute(db) + .await?; + } + _ => {} + } + Ok(()) +} + +async fn update_flow_status_module_with_actions_success( + db: &DB, + parent_job: &uuid::Uuid, + action_success: bool, +) -> Result<(), Error> { + let step = get_step_of_flow_status(db, parent_job.to_owned()).await?; + match step { + Step::Step(step) => { + // Append the new bool to the existing array, or create a new array if it doesn't exist + sqlx::query!( + r#" + UPDATE v2_job_status SET + flow_status = jsonb_set( + flow_status, + array['modules', $2::TEXT, 'agent_actions_success'], + COALESCE( + flow_status->'modules'->$2->'agent_actions_success', + to_jsonb(ARRAY[]::bool[]) + ) || to_jsonb(ARRAY[$3::bool]) + ) + WHERE id = $1 + "#, + parent_job, + step as i32, + action_success + ) + .execute(db) + .await?; + } + _ => {} + } + Ok(()) +} + +fn parse_raw_script_schema(content: &str, language: &ScriptLang) -> Result, Error> { + let main_arg_signature = parse_sig_of_lang(content, Some(&language), None)?.unwrap(); // safe to unwrap as langauge is some + + let schema = OpenAPISchema { + r#type: "object".to_string(), + properties: Some( + main_arg_signature + .args + .iter() + .map(|arg| { + let name = arg.name.clone(); + let typ = OpenAPISchema::from_typ(&arg.typ); + (name, Box::new(typ)) + }) + .collect(), + ), + required: Some( + main_arg_signature + .args + .iter() + .map(|arg| arg.name.clone()) + .collect(), + ), + ..Default::default() + }; + + Ok(to_raw_value(&schema)) +} + +#[async_recursion] // we only need it because handle_queued_job could call this function again but in practice it won't because we only accept workspace/raw script flow modules +async fn call_tool( + // connection + db: &DB, + conn: &Connection, + + // agent job and flow step id + agent_job: &MiniPulledJob, + + // tool + tool_module: &FlowModule, + tool_call: &OpenAIToolCall, + job_id: uuid::Uuid, + + // execution context + client: &AuthedClient, + occupancy_metrics: &mut OccupancyMetrics, + base_internal_url: &str, + worker_dir: &str, + worker_name: &str, + hostname: &str, + job_completed_tx: &JobCompletedSender, + killpill_rx: &mut tokio::sync::broadcast::Receiver<()>, +) -> error::Result<(bool, Arc>)> { + let tool_call_args = + serde_json::from_str::>>(&tool_call.function.arguments)?; + + let job_payload = match tool_module.get_value()? { + FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => { + let payload = script_to_payload( + script_hash, + script_path, + db, + agent_job, + tool_module, + tag_override, + tool_module.apply_preprocessor, + ) + .await?; + payload + } + FlowModuleValue::RawScript { + path, + content, + language, + lock, + tag, + custom_concurrency_key, + concurrent_limit, + concurrency_time_window_s, + .. + } => { + let path = path.unwrap_or_else(|| { + format!("{}/tools/{}", agent_job.runnable_path(), tool_module.id) + }); + + let payload = raw_script_to_payload( + path, + content, + language, + lock, + custom_concurrency_key, + concurrent_limit, + concurrency_time_window_s, + tool_module, + tag, + tool_module.delete_after_use.unwrap_or(false), + ); + payload + } + _ => { + return Err(Error::internal_err(format!( + "Unsupported tool: {}", + tool_call.function.name + ))); + } + }; + + let mut tx = db.begin().await?; + + let job_perms = get_job_perms(&mut *tx, &agent_job.id, &agent_job.workspace_id) + .await? + .map(|x| x.into()); + + let (email, permissioned_as) = if let Some(on_behalf_of) = job_payload.on_behalf_of.as_ref() { + (&on_behalf_of.email, on_behalf_of.permissioned_as.clone()) + } else { + ( + &agent_job.permissioned_as_email, + agent_job.permissioned_as.to_owned(), + ) + }; + + let job_priority = tool_module.priority.or(agent_job.priority); + + let tx = PushIsolationLevel::Transaction(tx); + let (uuid, tx) = push( + db, + tx, + &agent_job.workspace_id, + job_payload.payload, + PushArgs { args: &tool_call_args, extra: None }, + &agent_job.created_by, + email, + permissioned_as, + Some(&format!("job-span-{}", agent_job.id)), + None, + agent_job.schedule_path(), + Some(agent_job.id), + None, + Some(job_id), + false, + false, + None, + agent_job.visible_to_owner, + Some(agent_job.tag.clone()), // we reuse the same tag as the agent job because it's run on the same worker + job_payload.timeout, + None, + job_priority, + job_perms.as_ref(), + true, + ) + .await?; + + tx.commit().await?; + + let tool_job = get_mini_pulled_job(db, &uuid).await?; + + let Some(tool_job) = tool_job else { + return Err(Error::internal_err("Tool job not found".to_string())); + }; + + let tool_job = Arc::new(tool_job); + + let job_dir = create_job_dir(&worker_dir, agent_job.id).await; + + let (inner_job_completed_tx, inner_job_completed_rx) = JobCompletedSender::new(&conn, 1); + + let inner_job_completed_rx = inner_job_completed_rx.expect( + "inner_job_completed_tx should be set as agent jobs are not supported on agent workers", + ); + + #[cfg(feature = "benchmark")] + let mut bench = BenchmarkIter::new(); + + match handle_queued_job( + tool_job.clone(), + None, + None, + None, + None, + conn, + client, + hostname, + worker_name, + worker_dir, + &job_dir, + None, + base_internal_url, + inner_job_completed_tx, + occupancy_metrics, + killpill_rx, + None, + #[cfg(feature = "benchmark")] + &mut bench, + ) + .await + { + Err(err) => { + let err_string = format!("{}: {}", err.name(), err.to_string()); + let err_json = error_to_value(&err); + let _ = handle_non_flow_job_error( + db, + &tool_job, + 0, + None, + err_string, + err_json, + worker_name, + ) + .await; + Err(err) + } + Ok(success) => { + let send_result = inner_job_completed_rx.bounded_rx.try_recv().ok(); + + let result = if let Some(SendResult { + result: SendResultPayload::JobCompleted(JobCompleted { result, .. }), + .. + }) = send_result.as_ref() + { + job_completed_tx + .send(send_result.as_ref().unwrap().result.clone(), true) + .await + .map_err(to_anyhow)?; + result + } else { + if let Some(send_result) = send_result { + job_completed_tx + .send(send_result.result, true) + .await + .map_err(to_anyhow)?; + } + return Err(Error::internal_err( + "Tool job completed but no result".to_string(), + )); + }; + + Ok((success, result.clone())) + } + } +} + +async fn run_agent( + // connection + db: &DB, + conn: &Connection, + + // agent job and flow data + job: &MiniPulledJob, + parent_job: &uuid::Uuid, + args: AIAgentArgs, + tools: Vec, + + // job execution context + client: &AuthedClient, + occupancy_metrics: &mut OccupancyMetrics, + job_completed_tx: &JobCompletedSender, + worker_dir: &str, + base_internal_url: &str, + worker_name: &str, + hostname: &str, + killpill_rx: &mut tokio::sync::broadcast::Receiver<()>, +) -> error::Result> { + let mut messages = vec![ + OpenAIMessage { + role: "system".to_string(), + content: Some(args.system_prompt), + ..Default::default() + }, + OpenAIMessage { + role: "user".to_string(), + content: Some(args.user_message), + ..Default::default() + }, + ]; + + let mut actions = vec![]; + + let mut content = None; + + let base_url = args.provider.get_base_url(); + let api_key = args.provider.get_api_key(); + + let tool_defs = if tools.is_empty() { + None + } else { + Some(tools.iter().map(|t| &t.def).collect()) + }; + + for i in 0..MAX_AGENT_ITERATIONS { + let response = { + let resp = HTTP_CLIENT + .post(format!("{}/chat/completions", base_url)) + .bearer_auth(api_key) + .json(&OpenAIRequest { + model: args.provider.get_model(), + messages: &messages, + tools: tool_defs.as_ref(), + temperature: args.temperature, + max_completion_tokens: args.max_completion_tokens, + }) + .send() + .await + .map_err(|e| Error::internal_err(format!("Failed to call API: {}", e)))?; + + match resp.error_for_status_ref() { + Ok(_) => resp, + Err(e) => { + let status = resp.status(); + let text = resp + .text() + .await + .unwrap_or_else(|_| "".to_string()); + tracing::error!( + "Non 200 response from API: status: {}, body: {}", + status, + text + ); + return Err(Error::internal_err(format!( + "Non 200 response from API: {} - {}", + e, text + ))); + } + } + }; + + let mut response = response + .json::() + .await + .map_err(|e| Error::internal_err(format!("Failed to parse API response: {}", e)))?; + + let first_choice = response + .choices + .pop() + .ok_or_else(|| Error::internal_err("No response from API"))?; + + content = first_choice.message.content; + let tool_calls = first_choice.message.tool_calls.unwrap_or_default(); + + if let Some(ref content) = content { + actions.push(AgentAction::Message {}); + messages.push(OpenAIMessage { + role: "assistant".to_string(), + content: Some(content.clone()), + agent_action: Some(AgentAction::Message {}), + ..Default::default() + }); + + update_flow_status_module_with_actions(db, parent_job, &actions).await?; + update_flow_status_module_with_actions_success(db, parent_job, true).await?; + } + + if tool_calls.is_empty() { + break; + } else if i == MAX_AGENT_ITERATIONS - 1 { + return Err(Error::internal_err( + "AI agent reached max iterations, but there are still tool calls".to_string(), + )); + } + + messages.push(OpenAIMessage { + role: "assistant".to_string(), + tool_calls: Some(tool_calls.clone()), + ..Default::default() + }); + + for tool_call in tool_calls.iter() { + let tool = tools + .iter() + .find(|t| t.def.function.name == tool_call.function.name); + if let Some(tool) = tool { + let job_id = ulid::Ulid::new().into(); + actions.push(AgentAction::ToolCall { + job_id, + function_name: tool_call.function.name.clone(), + module_id: tool.module.id.clone(), + }); + + update_flow_status_module_with_actions(db, parent_job, &actions).await?; + + match call_tool( + db, + conn, + job, + &tool.module, + &tool_call, + job_id, + client, + occupancy_metrics, + base_internal_url, + worker_dir, + worker_name, + hostname, + job_completed_tx, + killpill_rx, + ) + .await + { + Ok((success, result)) => { + messages.push(OpenAIMessage { + role: "tool".to_string(), + content: Some(result.get().to_string()), + tool_call_id: Some(tool_call.id.clone()), + agent_action: Some(AgentAction::ToolCall { + job_id, + function_name: tool_call.function.name.clone(), + module_id: tool.module.id.clone(), + }), + ..Default::default() + }); + update_flow_status_module_with_actions_success(db, parent_job, success) + .await?; + } + Err(err) => { + let err_string = format!("{}: {}", err.name(), err.to_string()); + messages.push(OpenAIMessage { + role: "tool".to_string(), + content: Some(format!("Error running tool: {}", err_string)), + tool_call_id: Some(tool_call.id.clone()), + agent_action: Some(AgentAction::ToolCall { + job_id, + function_name: tool_call.function.name.clone(), + module_id: tool.module.id.clone(), + }), + ..Default::default() + }); + update_flow_status_module_with_actions_success(db, parent_job, false) + .await?; + } + } + } else { + return Err(Error::internal_err(format!( + "Tool not found: {}", + tool_call.function.name + ))); + } + } + } + + let final_messages: Vec = messages + .iter() + .map(|m| Message { message: m, agent_action: m.agent_action.as_ref() }) + .collect(); + + Ok(to_raw_value(&AIAgentResult { + output: content.unwrap_or_default().clone(), + messages: final_messages, + })) +} + +pub struct FlowJobRunnableIdAndRawFlow { + pub runnable_id: Option, + pub raw_flow: Option>>, + pub kind: JobKind, +} + +pub async fn get_flow_job_runnable_and_raw_flow( + db: &DB, + job_id: &uuid::Uuid, +) -> windmill_common::error::Result { + let job = sqlx::query_as!( + FlowJobRunnableIdAndRawFlow, + "SELECT runnable_id as \"runnable_id: ScriptHash\", raw_flow as \"raw_flow: _\", kind as \"kind: _\" FROM v2_job WHERE id = $1", + job_id + ) + .fetch_one(db) + .await?; + Ok(job) +} + +pub async fn handle_ai_agent_job( + // connection + conn: &Connection, + db: &DB, + + // agent job + job: &MiniPulledJob, + + // job execution context + client: &AuthedClient, + canceled_by: &mut Option, + mem_peak: &mut i32, + occupancy_metrics: &mut OccupancyMetrics, + job_completed_tx: &JobCompletedSender, + worker_dir: &str, + base_internal_url: &str, + worker_name: &str, + hostname: &str, + killpill_rx: &mut tokio::sync::broadcast::Receiver<()>, +) -> Result, Error> { + let args = build_args_map(job, client, conn).await?; + + let args = serde_json::from_str::(&serde_json::to_string(&args)?)?; + + let Some(flow_step_id) = &job.flow_step_id else { + return Err(Error::internal_err( + "AI agent job has no flow step id".to_string(), + )); + }; + + let Some(parent_job) = &job.parent_job else { + return Err(Error::internal_err( + "AI agent job has no parent job".to_string(), + )); + }; + + let flow_job = get_flow_job_runnable_and_raw_flow(db, &parent_job).await?; + + let flow_data = match flow_job.kind { + JobKind::Flow | JobKind::FlowNode => { + cache::job::fetch_flow(db, &flow_job.kind, flow_job.runnable_id).await? + } + JobKind::FlowPreview => { + cache::job::fetch_preview_flow(db, &parent_job, flow_job.raw_flow).await? + } + _ => { + return Err(Error::internal_err( + "expected parent flow, flow preview or flow node for ai agent job".to_string(), + )); + } + }; + + let value = flow_data.value(); + + let module = value.modules.iter().find(|m| m.id == *flow_step_id); + + let Some(module) = module else { + return Err(Error::internal_err( + "AI agent module not found in flow".to_string(), + )); + }; + + let FlowModuleValue::AIAgent { tools, .. } = module.get_value()? else { + return Err(Error::internal_err( + "AI agent module is not an AI agent".to_string(), + )); + }; + + let tools = futures::future::try_join_all(tools.into_iter().map(|mut t| { + let conn = conn; + let db = db; + let job = job; + async move { + let Some(summary) = t.summary.as_ref().filter(|s| TOOL_NAME_REGEX.is_match(s)) else { + return Err(Error::internal_err(format!( + "Invalid tool name: {:?}", + t.summary + ))); + }; + + let schema = match &t.get_value() { + Ok(FlowModuleValue::Script { + hash, + path, + tag_override, + input_transforms, + is_trigger, + }) => match hash { + Some(hash) => { + let (_, metadata) = cache::script::fetch(conn, hash.clone()).await?; + Ok::<_, Error>( + metadata + .schema + .clone() + .map(|s| RawValue::from_string(s).ok()) + .flatten(), + ) + } + None => { + if path.starts_with("hub/") { + let hub_script = get_full_hub_script_by_path( + StripPath(path.to_string()), + &HTTP_CLIENT, + None, + ) + .await?; + Ok(Some(hub_script.schema)) + } else { + let hash = get_latest_hash_for_path(db, &job.workspace_id, path) + .await? + .0; + // update module definition to use a fixed hash so all tool calls match the same schema + t.value = to_raw_value(&FlowModuleValue::Script { + hash: Some(hash), + path: path.clone(), + tag_override: tag_override.clone(), + input_transforms: input_transforms.clone(), + is_trigger: *is_trigger, + }); + let (_, metadata) = cache::script::fetch(conn, hash).await?; + Ok(metadata + .schema + .clone() + .map(|s| RawValue::from_string(s).ok()) + .flatten()) + } + } + }, + Ok(FlowModuleValue::RawScript { content, language, .. }) => { + Ok(Some(parse_raw_script_schema(&content, &language)?)) + } + Err(e) => { + return Err(Error::internal_err(format!( + "Invalid tool {}: {}", + summary, + e.to_string() + ))); + } + _ => { + return Err(Error::internal_err(format!( + "Unsupported tool: {}", + summary + ))); + } + }?; + + Ok(Tool { + def: ToolDef { + r#type: "function".to_string(), + function: ToolDefFunction { + name: summary.clone(), + parameters: schema.unwrap_or_else(|| { + to_raw_value(&serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })) + }), + }, + }, + module: t, + }) + } + })) + .await?; + + let mut inner_occupancy_metrics = occupancy_metrics.clone(); + + let agent_fut = run_agent( + db, + conn, + job, + parent_job, + args, + tools, + client, + &mut inner_occupancy_metrics, + job_completed_tx, + worker_dir, + base_internal_url, + worker_name, + hostname, + killpill_rx, + ); + + let result = run_future_with_polling_update_job_poller( + job.id, + job.timeout, + conn, + mem_peak, + canceled_by, + agent_fut, + worker_name, + &job.workspace_id, + &mut Some(occupancy_metrics), + Box::pin(futures::stream::once(async { 0 })), + ) + .await?; + + Ok(result) +} diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index b9083a5119..9f81ce798e 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -758,7 +758,7 @@ pub async fn prebundle_bun_script( pub const BUN_BUNDLE_OBJECT_STORE_PREFIX: &str = "bun_bundle/"; async fn get_script_import_updated_at(db: &DB, w_id: &str, script_path: &str) -> Result { - let script_hash = get_latest_hash_for_path(&mut db.begin().await?, w_id, script_path).await?; + let script_hash = get_latest_hash_for_path(db, w_id, script_path).await?; let last_updated_at = sqlx::query_scalar!( "SELECT created_at FROM script WHERE workspace_id = $1 AND hash = $2", w_id, diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 051101d727..2b7523ecad 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -549,13 +549,14 @@ pub async fn update_worker_ping_for_failed_init_script( } } -pub fn error_to_value(err: Error) -> serde_json::Value { +pub fn error_to_value(err: &Error) -> serde_json::Value { match err { - Error::JsonErr(err) => err, + Error::JsonErr(err) => err.clone(), _ => json!({"message": err.to_string(), "name": err.name()}), } } +#[derive(Clone)] pub struct OccupancyMetrics { pub running_job_started_at: Option, pub total_duration_of_running_jobs: f32, diff --git a/backend/windmill-worker/src/dedicated_worker.rs b/backend/windmill-worker/src/dedicated_worker.rs index 56b21f94d6..1539c33c5c 100644 --- a/backend/windmill-worker/src/dedicated_worker.rs +++ b/backend/windmill-worker/src/dedicated_worker.rs @@ -433,6 +433,7 @@ async fn spawn_dedicated_workers_for_flow( } FlowModuleValue::Flow { .. } => (), FlowModuleValue::Identity => (), + FlowModuleValue::AIAgent { .. } => (), } } else { tracing::error!("failed to get value for module: {:?}", module); diff --git a/backend/windmill-worker/src/go_executor.rs b/backend/windmill-worker/src/go_executor.rs index 1f09f325de..8fe187b8dd 100644 --- a/backend/windmill-worker/src/go_executor.rs +++ b/backend/windmill-worker/src/go_executor.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use windmill_common::{ error::{self, Error}, utils::calculate_hash, - worker::{save_cache, write_file, Connection}, + worker::{save_cache, write_file, Connection, GoAnnotations}, }; use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE}; use windmill_queue::{append_logs, CanceledBy, MiniPulledJob}; @@ -464,6 +464,7 @@ pub async fn install_go_dependencies( w_id: &str, occupation_metrics: &mut OccupancyMetrics, ) -> error::Result { + let anns = GoAnnotations::parse(code); if raw_deps { let go_mod = if let Some(module) = code.lines().find(|l| l.trim_start().starts_with("module ")) { @@ -529,7 +530,11 @@ pub async fn install_go_dependencies( } else { "".to_string() }; - let hash = format!("go-{}", hash); + let hash = format!( + "go{}-{}", + if anns.go1_22_compat { "1.22" } else { "" }, + hash + ); let mut skip_tidy = has_sum; @@ -579,6 +584,11 @@ pub async fn install_go_dependencies( .args(vec!["mod", mod_command]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + // If annotation used we want to call tidy with special flag to pin go to 1.22 + // The reason for this that at some point we had to jump from go 1.22 to 1.25 and this addds backward compatibility. + if anns.go1_22_compat && mod_command == "tidy" { + child_cmd.args(vec!["-go", "1.22"]); + } #[cfg(windows)] set_windows_env_vars(&mut child_cmd); diff --git a/backend/windmill-worker/src/lib.rs b/backend/windmill-worker/src/lib.rs index e917e32fba..3972de49e7 100644 --- a/backend/windmill-worker/src/lib.rs +++ b/backend/windmill-worker/src/lib.rs @@ -16,6 +16,7 @@ mod java_executor; #[cfg(feature = "ruby")] mod ruby_executor; +mod ai_executor; mod bun_executor; pub mod common; mod config; diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 354ff0b8dc..eb6a6e7f30 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -852,6 +852,7 @@ mount {{ { python_cmd.env("SystemRoot", SYSTEM_ROOT.as_str()); python_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str()); + python_cmd.env("windir", SYSTEM_ROOT.as_str()); python_cmd.env( "LOCALAPPDATA", std::env::var("LOCALAPPDATA") diff --git a/backend/windmill-worker/src/result_processor.rs b/backend/windmill-worker/src/result_processor.rs index cf455e3f35..2fade141bf 100644 --- a/backend/windmill-worker/src/result_processor.rs +++ b/backend/windmill-worker/src/result_processor.rs @@ -647,7 +647,7 @@ pub async fn process_completed_job( return Ok(None); } -async fn handle_non_flow_job_error( +pub async fn handle_non_flow_job_error( db: &DB, job: &MiniPulledJob, mem_peak: i32, @@ -692,7 +692,7 @@ pub async fn handle_job_error( #[cfg(feature = "benchmark")] bench: &mut BenchmarkIter, ) { let err_string = format!("{}: {}", err.name(), err.to_string()); - let err_json = error_to_value(err); + let err_json = error_to_value(&err); let update_job_future = || async { handle_non_flow_job_error( diff --git a/backend/windmill-worker/src/snowflake_executor.rs b/backend/windmill-worker/src/snowflake_executor.rs index 8828aeb76b..fd85e683fa 100644 --- a/backend/windmill-worker/src/snowflake_executor.rs +++ b/backend/windmill-worker/src/snowflake_executor.rs @@ -20,13 +20,13 @@ use windmill_queue::{CanceledBy, MiniPulledJob, HTTP_CLIENT}; use serde::{Deserialize, Serialize}; +use crate::common::build_args_values; use crate::common::{ build_http_client, resolve_job_timeout, s3_mode_args_to_worker_data, OccupancyMetrics, S3ModeWorkerData, }; use crate::handle_child::run_future_with_polling_update_job_poller; use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args; -use crate::common::build_args_values; use windmill_common::client::AuthedClient; #[derive(Serialize)] diff --git a/backend/windmill-worker/src/universal_pkg_installer.rs b/backend/windmill-worker/src/universal_pkg_installer.rs index 1e4a8c2d39..dc4b5dab2a 100644 --- a/backend/windmill-worker/src/universal_pkg_installer.rs +++ b/backend/windmill-worker/src/universal_pkg_installer.rs @@ -14,6 +14,7 @@ use windmill_common::{error, worker::Connection}; use crate::{common::start_child_process, DISABLE_NSJAIL}; +#[allow(dead_code)] #[derive(Clone, Debug)] pub struct RequiredDependency { /// Expected directory of dependency in cache diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 2ed78f0066..46522388e6 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -99,6 +99,7 @@ use tokio::{ use rand::Rng; +use crate::ai_executor::handle_ai_agent_job; use crate::{ agent_workers::{queue_init_job, queue_periodic_job}, bash_executor::{handle_bash_job, handle_powershell_job}, @@ -753,7 +754,7 @@ pub async fn handle_all_job_kind_error( preprocessed_args: None, job: job.clone(), result: Arc::new(windmill_common::worker::to_raw_value(&error_to_value( - err, + &err, ))), result_columns: None, mem_peak: 0, @@ -899,7 +900,7 @@ pub fn start_interactive_worker_shell( }) } -async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String { +pub async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String { let job_dir_path = format!("{}/{}", worker_directory, job_id); create_directory_async(&job_dir_path).await; @@ -2190,11 +2191,13 @@ pub struct SendResult { pub time: Instant, } +#[derive(Clone)] pub enum SendResultPayload { JobCompleted(JobCompleted), UpdateFlow(UpdateFlow), } +#[derive(Clone)] pub struct UpdateFlow { pub flow: Uuid, pub w_id: String, @@ -2341,9 +2344,10 @@ pub async fn handle_queued_job( | JobKind::FlowDependencies, x, ) => match x.map(|x| x.0) { - None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => { - Some(cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow).await?) - } + None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => Some( + cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone()) + .await?, + ), _ => None, }, _ => None, @@ -2548,6 +2552,31 @@ pub async fn handle_queued_job( .flatten() .map(|x| x.to_owned()) .unwrap_or_else(|| serde_json::from_str("{}").unwrap())), + JobKind::AIAgent => match conn { + Connection::Sql(db) => { + handle_ai_agent_job( + conn, + db, + job.as_ref(), + &client, + &mut canceled_by, + &mut mem_peak, + &mut *occupancy_metrics, + &job_completed_tx, + worker_dir, + base_internal_url, + worker_name, + hostname, + killpill_rx, + ) + .await + } + Connection::Http(_) => { + return Err(Error::internal_err( + "Agent worker does not support ai agent jobs".to_string(), + )); + } + }, _ => { let metric_timer = Instant::now(); let preview_data = preview_data.and_then(|data| match data { @@ -2732,6 +2761,7 @@ async fn try_validate_schema( JobKind::AppDependencies => 12, JobKind::Noop => 13, JobKind::FlowNode => 14, + JobKind::AIAgent => 15, }; let sv = match job.runnable_id { @@ -3488,7 +3518,7 @@ mount {{ result } -fn parse_sig_of_lang( +pub fn parse_sig_of_lang( code: &str, language: Option<&ScriptLang>, main_override: Option, diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 6abe8e57ad..37c6e3fc70 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -29,7 +29,7 @@ use sqlx::types::Json; use sqlx::{FromRow, Postgres, Transaction}; use tracing::instrument; use uuid::Uuid; -use windmill_common::auth::JobPerms; +use windmill_common::auth::get_job_perms; #[cfg(feature = "benchmark")] use windmill_common::bench::BenchmarkIter; use windmill_common::cache::{self, RawData}; @@ -626,6 +626,8 @@ pub async fn update_flow_status_after_job_completion_internal( approvers: vec![], failed_retries: vec![], skipped: false, + agent_actions: None, + agent_actions_success: None, } } else { success = false; @@ -636,6 +638,8 @@ pub async fn update_flow_status_after_job_completion_internal( flow_jobs_success: flow_jobs_success.clone(), branch_chosen: None, failed_retries: vec![], + agent_actions: None, + agent_actions_success: None, } }; let r = sqlx::query_scalar!( @@ -806,6 +810,8 @@ pub async fn update_flow_status_after_job_completion_internal( approvers: vec![], failed_retries: old_status.retry.failed_jobs.clone(), skipped: is_skipped, + agent_actions: module_status.agent_actions(), + agent_actions_success: module_status.agent_actions_success(), }), ) } else { @@ -829,6 +835,8 @@ pub async fn update_flow_status_after_job_completion_internal( flow_jobs_success, branch_chosen, failed_retries: old_status.retry.failed_jobs.clone(), + agent_actions: module_status.agent_actions(), + agent_actions_success: module_status.agent_actions_success(), }), ) } @@ -2517,7 +2525,8 @@ async fn push_next_flow_job( FlowModuleValue::Script { input_transforms, .. } | FlowModuleValue::RawScript { input_transforms, .. } | FlowModuleValue::FlowScript { input_transforms, .. } - | FlowModuleValue::Flow { input_transforms, .. }, + | FlowModuleValue::Flow { input_transforms, .. } + | FlowModuleValue::AIAgent { input_transforms, .. }, ) => { let ctx = get_transform_context(&flow_job, &previous_id, &status) .warn_after_seconds(3) @@ -2605,6 +2614,8 @@ async fn push_next_flow_job( approvers: vec![], failed_retries: vec![], skipped: false, + agent_actions: None, + agent_actions_success: None, }), flow_job.id ) @@ -2825,16 +2836,9 @@ async fn push_next_flow_job( .flow_innermost_root_job .or_else(|| Some(flow_job.id)) { - sqlx::query_as!( - JobPerms, - "SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2", - root_job, - flow_job.workspace_id, - ) - .fetch_optional(&mut *tx) - .warn_after_seconds(3) - .await? - .map(|x| x.into()) + get_job_perms(&mut *tx, root_job, &flow_job.workspace_id) + .await? + .map(|x| x.into()) } else { None } @@ -2886,6 +2890,7 @@ async fn push_next_flow_job( Some(module.id.clone()), new_job_priority_override, job_perms.as_ref(), + false, ) .warn_after_seconds(2) .await?; @@ -3001,6 +3006,8 @@ async fn push_next_flow_job( parallel: false, while_loop, progress: None, + agent_actions: None, + agent_actions_success: None, } } NextStatus::AllFlowJobs { iterator, branchall, .. } => FlowStatusModule::InProgress { @@ -3014,6 +3021,8 @@ async fn push_next_flow_job( parallel: true, while_loop: false, progress: None, + agent_actions: None, + agent_actions_success: None, }, NextStatus::NextBranchStep(NextBranch { mut flow_jobs, @@ -3037,6 +3046,8 @@ async fn push_next_flow_job( parallel: false, while_loop: false, progress: None, + agent_actions: None, + agent_actions_success: None, } } @@ -3051,6 +3062,8 @@ async fn push_next_flow_job( parallel: false, while_loop: false, progress: None, + agent_actions: None, + agent_actions_success: None, }, NextStatus::NextStep => { FlowStatusModule::WaitingForExecutor { id: status_module.id(), job: one_uuid? } @@ -3253,12 +3266,12 @@ enum NextStatus { } #[derive(Clone)] -struct JobPayloadWithTag { - payload: JobPayload, - tag: Option, - delete_after_use: bool, - timeout: Option, - on_behalf_of: Option, +pub struct JobPayloadWithTag { + pub payload: JobPayload, + pub tag: Option, + pub delete_after_use: bool, + pub timeout: Option, + pub on_behalf_of: Option, } enum ContinuePayload { SingleJob(JobPayloadWithTag), @@ -3320,7 +3333,7 @@ fn payload_from_modules<'a>( }) } -fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModule) -> String { +pub fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModule) -> String { if status .preprocessor_module .as_ref() @@ -3390,6 +3403,20 @@ async fn compute_next_flow_transform( NextStatus::NextStep, )) } + FlowModuleValue::AIAgent { .. } => { + let path = get_path(flow_job, status, module); + let payload = JobPayload::AIAgent { path }; + Ok(NextFlowTransform::Continue( + ContinuePayload::SingleJob(JobPayloadWithTag { + payload, + tag: None, + delete_after_use, + timeout: None, + on_behalf_of: None, + }), + NextStatus::NextStep, + )) + } FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => { let payload = script_to_payload( script_hash, @@ -4087,7 +4114,7 @@ async fn payload_from_simple_module( }) } -fn raw_script_to_payload( +pub fn raw_script_to_payload( path: String, content: String, language: windmill_common::scripts::ScriptLang, @@ -4137,7 +4164,7 @@ async fn flow_to_payload( Ok(JobPayloadWithTag { payload, tag, delete_after_use, timeout: None, on_behalf_of }) } -async fn script_to_payload( +pub async fn script_to_payload( script_hash: Option, script_path: String, db: &sqlx::Pool, diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 575d94c28c..38e3edec4d 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -627,6 +627,7 @@ async fn trigger_dependents_to_recompute_dependencies( None, None, None, + false, ) .await?; tracing::info!( diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 1267aa0e52..edf4971ef8 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.527.1"; +export const VERSION = "v1.532.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index baf95987e6..3c21060113 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -68,7 +68,7 @@ export { // } // }); -export const VERSION = "1.527.1"; +export const VERSION = "1.532.0"; const command = new Command() .name("wmill") diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 25c9e2a219..d55cbfc930 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -21,7 +21,7 @@ import { import { inferContentTypeFromFilePath } from "./script_common.ts"; import { GlobalDeps, exts, findGlobalDeps } from "../commands/script/script.ts"; import { FSFSElement, findCodebase, yamlOptions } from "../commands/sync/sync.ts"; -import { generateHash, readInlinePathSync } from "./utils.ts"; +import { generateHash, readInlinePathSync, getHeaders } from "./utils.ts"; import { SyncCodebase } from "./codebase.ts"; import { FlowFile } from "../commands/flow/flow.ts"; import { replaceInlineScripts } from "../../windmill-utils-internal/src/inline-scripts/replacer.ts"; @@ -384,6 +384,7 @@ async function updateScriptLock( } // generate the script lock running a dependency job in Windmill and update it inplace // TODO: update this once the client is released + const extraHeaders = getHeaders(); const rawResponse = await fetch( `${workspace.remote}api/w/${workspace.workspaceId}/jobs/run/dependencies`, { @@ -391,6 +392,7 @@ async function updateScriptLock( headers: { Cookie: `token=${workspace.token}`, "Content-Type": "application/json", + ...extraHeaders, }, body: JSON.stringify({ raw_scripts: [ @@ -455,6 +457,7 @@ export async function updateFlow( log.info(colors.blue("Using raw requirements for flow dependencies")); // generate the script lock running a dependency job in Windmill and update it inplace + const extraHeaders = getHeaders(); rawResponse = await fetch( `${workspace.remote}api/w/${workspace.workspaceId}/jobs/run/flow_dependencies`, { @@ -462,6 +465,7 @@ export async function updateFlow( headers: { Cookie: `token=${workspace.token}`, "Content-Type": "application/json", + ...extraHeaders, }, body: JSON.stringify({ flow_value, @@ -473,6 +477,7 @@ export async function updateFlow( ); } else { // Standard dependency resolution on the server + const extraHeaders = getHeaders(); rawResponse = await fetch( `${workspace.remote}api/w/${workspace.workspaceId}/jobs/run/flow_dependencies`, { @@ -480,6 +485,7 @@ export async function updateFlow( headers: { Cookie: `token=${workspace.token}`, "Content-Type": "application/json", + ...extraHeaders, }, body: JSON.stringify({ flow_value, diff --git a/docker-compose.yml b/docker-compose.yml index 82c66a9e69..f8831523ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,6 @@ services: - db_data:/var/lib/postgresql/data expose: - 5432 - ports: - - 5432:5432 environment: POSTGRES_PASSWORD: changeme POSTGRES_DB: windmill diff --git a/flake.nix b/flake.nix index be958fd547..8f4c3bca52 100644 --- a/flake.nix +++ b/flake.nix @@ -148,6 +148,13 @@ # LSP/Local dev svelte-language-server taplo + + # Orchestration/Kubernetes + minikube + kubectl + kubernetes-helm + conntrack-tools # To run minikube without driver (--driver=none) + cri-tools ]); packages = [ (pkgs.writeScriptBin "wm-caddy" '' @@ -245,6 +252,7 @@ ANSIBLE_PLAYBOOK_PATH = "${pkgs.ansible}/bin/ansible-playbook"; ANSIBLE_GALAXY_PATH = "${pkgs.ansible}/bin/ansible-galaxy"; # RUST_LOG = "debug"; + # RUST_LOG = "kube=debug"; SQLX_OFFLINE = "true"; # See this issue: https://github.com/NixOS/nixpkgs/issues/370494 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 19e959f53b..ab97bcb6cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.527.1", + "version": "1.532.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.527.1", + "version": "1.532.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 8babcbdb71..a61e8a233b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.527.1", + "version": "1.532.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/frontend/src/lib/components/AIAgentLogViewer.svelte b/frontend/src/lib/components/AIAgentLogViewer.svelte new file mode 100644 index 0000000000..2640ea52eb --- /dev/null +++ b/frontend/src/lib/components/AIAgentLogViewer.svelte @@ -0,0 +1,156 @@ + + +{#if job} +
+ {}} + mode="aiagent" + /> +
+{/if} diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 1a9e7c0efc..a15c461c12 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -6,6 +6,7 @@ import { setInputCat as computeInputCat, debounce, + emptySchema, emptyString, getSchemaFromProperties, type DynamicSelect @@ -42,6 +43,8 @@ import { safeSelectItems } from './select/utils.svelte' import S3ArgInput from './common/fileUpload/S3ArgInput.svelte' import { base } from '$lib/base' + import { workspaceStore } from '$lib/stores' + import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte' interface Props { label?: string @@ -658,6 +661,47 @@ {appPath} {computeS3ForceViewerPolicies} /> + {:else if inputCat == 'object' && format == 'json-schema'} + {#await import('$lib/components/EditableSchemaForm.svelte')} + + {:then Module} + + value && typeof value === 'object' && !Array.isArray(value) ? value : emptySchema(), + (v) => { + value = v + } + } + isFlowInput + editTab="inputEditor" + noPreview + addPropertyInEditorTab + /> + {/await} + {:else if inputCat == 'object' && format?.startsWith('jsonschema-')} + {#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $workspaceStore ?? '')} + + {:then schema} + {#if !schema || !schema.properties} + {#await import('$lib/components/JsonEditor.svelte')} + + {:then Module} + + {/await} + {:else} +
+ +
+ {/if} + {/await} {:else if inputCat == 'list' && !isListJson}
diff --git a/frontend/src/lib/components/AutoscalingConfigEditor.svelte b/frontend/src/lib/components/AutoscalingConfigEditor.svelte index ad7cd5294e..74215e9aaa 100644 --- a/frontend/src/lib/components/AutoscalingConfigEditor.svelte +++ b/frontend/src/lib/components/AutoscalingConfigEditor.svelte @@ -11,6 +11,7 @@ import Label from './Label.svelte' import MultiSelect from './select/MultiSelect.svelte' import { safeSelectItems } from './select/utils.svelte' + import { ConfigService } from '$lib/gen' interface Props { config: AutoscalingConfig | undefined @@ -21,6 +22,27 @@ const dispatch = createEventDispatcher() let test_input: number = $state(3) + let healthCheckLoading: boolean = $state(false) + let healthCheckResult: { success: boolean; error?: string } | null = $state(null) + + async function checkKubernetesHealth() { + if (!config?.integration || config.integration.type !== 'kubernetes') return + + healthCheckLoading = true + healthCheckResult = null + + try { + await ConfigService.nativeKubernetesAutoscalingHealthcheck() + healthCheckResult = { success: true } + } catch (error: any) { + healthCheckResult = { + success: false, + error: error.body || error.message || 'Unknown error' + } + } finally { + healthCheckLoading = false + } + }
@@ -262,7 +284,7 @@ /> - + {/snippet} @@ -324,6 +346,58 @@
{/if} + + {#if config.integration.type === 'kubernetes'} +
+ Kubernetes configuration is automatically inferred from the cluster environment. + The worker group name and namespace will be detected automatically. +
+ +
+
+ + +
+ + {#if healthCheckResult !== null} +
+ {#if healthCheckResult.success} + Kubernetes autoscaling is healthy + {:else} + {healthCheckResult.error} + {#if healthCheckResult.error?.includes('permissions') || healthCheckResult.error?.includes('role')} +
Please follow the setup guide above to configure proper RBAC permissions. + {/if} + {/if} +
+ {/if} + +
+ +
+ + workers +
+
+
+ {/if} {:else} {#snippet children({ item })} @@ -331,7 +405,7 @@ - + {/snippet} diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index d36a557b3d..4eececb8ef 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -17,7 +17,7 @@ } from '$lib/gen' import { inferArgs } from '$lib/infer' import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores' - import { emptySchema, readFieldsRecursively, sendUserToast } from '$lib/utils' + import { emptySchema, readFieldsRecursively, sendUserToast, type StateStore } from '$lib/utils' import { Pane, Splitpanes } from 'svelte-splitpanes' import { onDestroy, onMount, setContext, untrack } from 'svelte' import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte' @@ -27,7 +27,7 @@ import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte' import FlowEditorPanel from './flows/content/FlowEditorPanel.svelte' import { deepEqual } from 'fast-equals' - import { writable, type Writable } from 'svelte/store' + import { writable } from 'svelte/store' import type { FlowState } from './flows/flowState' import { initHistory } from '$lib/history.svelte' import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types' @@ -51,7 +51,6 @@ import { TestSteps } from './flows/testSteps.svelte' import { ModulesTestStates } from './modulesTest.svelte' import type { GraphModuleState } from './graph' - import { updateDerivedModuleStatesFromTestJobs } from './flows/utils' let flowCopilotContext: FlowCopilotContext = { shouldUpdatePropertyType: writable<{ @@ -116,7 +115,6 @@ const flowPreviewContent = $derived(flowPreviewButtons?.getFlowPreviewContent()) const job: Job | undefined = $derived(flowPreviewContent?.getJob()) let showJobStatus = $state(false) - let testModuleId: string | undefined = $state(undefined) type LastEditScript = { content: string @@ -453,7 +451,7 @@ } } - const flowStateStore = writable({} as FlowState) + const flowStateStore = $state({ val: {} }) as StateStore const previewArgsStore = $state({ val: {} }) const scriptEditorDrawer = writable(undefined) @@ -464,8 +462,6 @@ const triggersCount = writable(undefined) const modulesTestStates = new ModulesTestStates((moduleId) => { // Update the derived store with test job states - delete $derivedModuleStates[moduleId] - testModuleId = moduleId showJobStatus = false }) const outputPickerOpenFns: Record void> = $state({}) @@ -538,11 +534,11 @@ } mod.value.input_transforms = input_transforms - if (!deepEqual(schema, $flowStateStore[mod.id]?.schema)) { - if (!$flowStateStore[mod.id]) { - $flowStateStore[mod.id] = { schema } + if (!deepEqual(schema, flowStateStore.val[mod.id]?.schema)) { + if (!flowStateStore.val[mod.id]) { + flowStateStore.val[mod.id] = { schema } } else { - $flowStateStore[mod.id].schema = schema + flowStateStore.val[mod.id].schema = schema } reload++ } @@ -586,25 +582,12 @@ $selectedIdStore && untrack(() => inferModuleArgs($selectedIdStore)) }) - const localModuleStates: Writable> = $derived( - flowPreviewContent?.getLocalModuleStates() ?? writable({}) - ) + let localModuleStates: Record = $state({}) - const suspendStatus: Writable> = $derived( - flowPreviewContent?.getSuspendStatus() ?? writable({}) - ) + let suspendStatus: StateStore> = $state({ val: {} }) // Create a derived store that only shows the module states when showModuleStatus is true // this store can also be updated - let derivedModuleStates = writable>({}) - $effect(() => { - derivedModuleStates.update((currentStates) => { - return showJobStatus ? $localModuleStates : currentStates - }) - }) - $effect(() => { - updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates) - }) let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state() function onJobDone() { @@ -639,14 +622,9 @@ } function resetModulesStates() { - derivedModuleStates.set({}) showJobStatus = false } - const individualStepTests = $derived( - !(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0 - ) - const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged()) @@ -785,7 +763,7 @@ bind:this={flowPreviewButtons} {onJobDone} onRunPreview={() => { - localModuleStates.set({}) + localModuleStates = {} showJobStatus = true }} /> @@ -800,19 +778,20 @@ disableTutorials smallErrorHandler={true} disableStaticInputs - localModuleStates={derivedModuleStates} + {localModuleStates} onTestUpTo={flowPreviewButtons?.testUpTo} + testModuleStates={modulesTestStates} isOwner={flowPreviewContent?.getIsOwner?.()} onTestFlow={flowPreviewButtons?.runPreview} isRunning={flowPreviewContent?.getIsRunning?.()} onCancelTestFlow={flowPreviewContent?.cancelTest} onOpenPreview={flowPreviewButtons?.openPreview} onHideJobStatus={resetModulesStates} - {individualStepTests} flowJob={job} {showJobStatus} onDelete={(id) => { - delete $derivedModuleStates[id] + delete localModuleStates[id] + delete modulesTestStates.states[id] }} {flowHasChanged} /> diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index e2b9aebace..a0d5784552 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -928,7 +928,7 @@ > {:else if !s3object?.disable_download} - + {:else}
{s3object?.s3} (download disabled)
{/if} diff --git a/frontend/src/lib/components/EditableSchemaForm.svelte b/frontend/src/lib/components/EditableSchemaForm.svelte index d91cc5c2d6..a3dfc5a168 100644 --- a/frontend/src/lib/components/EditableSchemaForm.svelte +++ b/frontend/src/lib/components/EditableSchemaForm.svelte @@ -29,6 +29,7 @@ import type { EditableSchemaFormUi } from '$lib/components/custom_ui' import Section from '$lib/components/Section.svelte' import Editor from './Editor.svelte' + import AddPropertyV2 from './schema/AddPropertyV2.svelte' // export let openEditTab: () => void = () => {} const dispatch = createEventDispatcher() @@ -69,6 +70,7 @@ dynSelectCode?: string | undefined dynSelectLang?: ScriptLang | undefined showDynSelectOpt?: boolean + addPropertyInEditorTab?: boolean openEditTab?: import('svelte').Snippet addProperty?: import('svelte').Snippet runButton?: import('svelte').Snippet @@ -104,6 +106,7 @@ dynSelectCode = $bindable(), dynSelectLang = $bindable(), showDynSelectOpt = false, + addPropertyInEditorTab = false, openEditTab, addProperty, runButton, @@ -509,22 +512,31 @@ {:else} {#if jsonEnabled && customUi?.jsonOnly != true} -
- { - schemaString = JSON.stringify(schema, null, '\t') - editor?.setCode(schemaString) - }} - /> +
+ {#if addPropertyInEditorTab} + + {#snippet trigger()} +
{/if} @@ -655,7 +667,6 @@ const isS3 = v == 'S3' const isOneOf = v == 'oneOf' const isDynSelect = v == 'dynselect' - const emptyProperty = { contentEncoding: undefined, enum_: undefined, diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 825b6f7f58..1796ab76c0 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -100,7 +100,8 @@ codeCompletionSessionEnabled, lspTokenStore, formatOnSave, - vimMode + vimMode, + relativeLineNumbers } from '$lib/stores' import { editorConfig, updateOptions } from '$lib/editorUtils' @@ -183,6 +184,7 @@ loadAsync?: boolean key?: string | undefined class?: string | undefined + moduleId?: string } let { @@ -208,7 +210,8 @@ changeTimeout = 500, loadAsync = false, key = undefined, - class: clazz = undefined + class: clazz = undefined, + moduleId = undefined }: Props = $props() $effect.pre(() => { @@ -1234,7 +1237,7 @@ try { editor = meditor.create(divEl as HTMLDivElement, { - ...editorConfig(code ?? '', lang, automaticLayout, fixedOverflowWidgets), + ...editorConfig(code ?? '', lang, automaticLayout, fixedOverflowWidgets, $relativeLineNumbers), model, fontSize: !small ? 14 : 12, lineNumbersMinChars, @@ -1327,7 +1330,8 @@ aiChatManager.addSelectedLinesToContext( selectedLines, selection.startLineNumber, - selection.endLineNumber + selection.endLineNumber, + moduleId ) } else { aiChatManager.toggleOpen() @@ -1652,6 +1656,11 @@ $effect(() => { files && model && untrack(() => onFileChanges()) }) + $effect(() => { + editor?.updateOptions({ + lineNumbers: $relativeLineNumbers ? 'relative' : 'on' + }) + }) diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 40665e3f36..3dfe898301 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -82,6 +82,7 @@ showHistoryDrawer?: boolean right?: import('svelte').Snippet openAiChat?: boolean + moduleId?: string } let { @@ -105,7 +106,8 @@ diffMode = false, showHistoryDrawer = $bindable(false), right, - openAiChat = false + openAiChat = false, + moduleId = undefined }: Props = $props() let contextualVariablePicker: ItemPicker | undefined = $state() @@ -964,7 +966,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS {#if customUi?.aiGen != false} {#if openAiChat} - + {:else} {/if} diff --git a/frontend/src/lib/components/EditorSettings.svelte b/frontend/src/lib/components/EditorSettings.svelte index fe1bd4bdf9..88984a3a52 100644 --- a/frontend/src/lib/components/EditorSettings.svelte +++ b/frontend/src/lib/components/EditorSettings.svelte @@ -2,6 +2,7 @@ import { Settings } from 'lucide-svelte' import FormatOnSave from './FormatOnSave.svelte' import VimMode from './VimMode.svelte' + import RelativeLineNumbers from './RelativeLineNumbers.svelte' import { Button } from './common' import CodeCompletionStatus from './copilot/CodeCompletionStatus.svelte' import type { EditorBarUi } from './custom_ui' @@ -39,6 +40,11 @@
{/if} + {#if customUi?.relativeLineNumbers != false} +
+ +
+ {/if} {#if customUi?.aiCompletion != false}
diff --git a/frontend/src/lib/components/FieldHeader.svelte b/frontend/src/lib/components/FieldHeader.svelte index 9c465f73fe..15bc6638a6 100644 --- a/frontend/src/lib/components/FieldHeader.svelte +++ b/frontend/src/lib/components/FieldHeader.svelte @@ -40,7 +40,7 @@ {/if} {#if displayType} - {#if format && !format.startsWith('resource')} + {#if format && !format.startsWith('resource') && !format.startsWith('jsonschema-')} {format} diff --git a/frontend/src/lib/components/FirstStepInputs.svelte b/frontend/src/lib/components/FirstStepInputs.svelte index bd71e6a4b3..68bc0afe8c 100644 --- a/frontend/src/lib/components/FirstStepInputs.svelte +++ b/frontend/src/lib/components/FirstStepInputs.svelte @@ -19,7 +19,7 @@ let mod: any | undefined = $state(undefined) async function loadSchema() { try { - const res = await getFirstStepSchema($flowStateStore, flowStore.val) + const res = await getFirstStepSchema(flowStateStore.val, flowStore.val) schema = res.schema mod = res.mod dispatch('connectFirstNode', { connectFirstNode: res.connectFirstNode }) @@ -28,7 +28,7 @@ } } $effect(() => { - flowStore.val && $flowStateStore && untrack(() => loadSchema()) + flowStore.val && flowStateStore.val && untrack(() => loadSchema()) }) function handleClick() { diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 0eb3802a40..0573c67c40 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -25,6 +25,7 @@ orderedJsonStringify, readFieldsRecursively, replaceFalseWithUndefined, + type StateStore, type Value } from '$lib/utils' import { sendUserToast } from '$lib/toast' @@ -33,7 +34,7 @@ import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte' import { onMount, setContext, untrack, type ComponentType } from 'svelte' - import { writable, type Writable } from 'svelte/store' + import { writable } from 'svelte/store' import CenteredPage from './CenteredPage.svelte' import { Badge, Button, UndoRedo } from './common' import FlowEditor from './flows/FlowEditor.svelte' @@ -42,7 +43,7 @@ import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte' import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte' import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types' - import { cleanInputs, updateDerivedModuleStatesFromTestJobs } from './flows/utils' + import { cleanInputs } from './flows/utils' import { Calendar, Pen, @@ -577,13 +578,7 @@ } let insertButtonOpen = writable(false) - let testModuleId: string | undefined = $state(undefined) - let modulesTestStates = new ModulesTestStates((moduleId) => { - // Update the derived store with test job states - delete $derivedModuleStates[moduleId] - testModuleId = moduleId - showJobStatus = false - }) + let modulesTestStates = new ModulesTestStates() let outputPickerOpenFns: Record void> = $state({}) let flowEditor: FlowEditor | undefined = $state(undefined) @@ -933,33 +928,8 @@ } } - const localModuleStates: Writable> = $derived( - flowPreviewContent?.getLocalModuleStates() ?? writable({}) - ) - const suspendStatus: Writable> = $derived( - flowPreviewContent?.getSuspendStatus() ?? writable({}) - ) - - // Create a derived store that only shows the module states when showModuleStatus is true - // this store can also be updated - let derivedModuleStates = writable>({}) - $effect(() => { - derivedModuleStates.update((currentStates) => { - return showJobStatus ? $localModuleStates : currentStates - }) - }) - $effect(() => { - updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates) - }) - - function resetModulesStates() { - derivedModuleStates.set({}) - showJobStatus = false - } - - const individualStepTests = $derived( - !(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0 - ) + let localModuleStates: Record = $state({}) + let suspendStatus: StateStore> = $state({ val: {} }) const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged()) @@ -1025,7 +995,7 @@ for (const mod of restoredModules) { if (mod) { try { - loadFlowModuleState(mod).then((state) => ($flowStateStore[mod.id] = state)) + loadFlowModuleState(mod).then((state) => (flowStateStore.val[mod.id] = state)) } catch (e) { console.error('Error loading state for restored node', e) } @@ -1155,10 +1125,12 @@ showCaptureHint.set(true) }} {onJobDone} + bind:localModuleStates bind:this={flowPreviewButtons} {loading} onRunPreview={() => { - localModuleStates.set({}) + modulesTestStates.hideJobsInGraph() + localModuleStates = {} showJobStatus = true }} /> @@ -1185,7 +1157,7 @@
- {#if $flowStateStore} + {#if flowStateStore.val} aiChatManager.toggleOpen()} onOpenPreview={flowPreviewButtons?.openPreview} - localModuleStates={derivedModuleStates} + localModuleStates={showJobStatus ? localModuleStates : {}} + {showJobStatus} + testModuleStates={modulesTestStates} isOwner={flowPreviewContent?.getIsOwner()} onTestFlow={flowPreviewButtons?.runPreview} isRunning={flowPreviewContent?.getIsRunning()} onCancelTestFlow={flowPreviewContent?.cancelTest} - onHideJobStatus={resetModulesStates} - {individualStepTests} + onHideJobStatus={() => { + modulesTestStates.hideJobsInGraph() + showJobStatus = false + }} {job} {suspendStatus} - {showJobStatus} onDelete={(id) => { - delete $derivedModuleStates[id] + delete localModuleStates[id] + delete modulesTestStates.states[id] }} {flowHasChanged} /> diff --git a/frontend/src/lib/components/FlowGraphViewerStep.svelte b/frontend/src/lib/components/FlowGraphViewerStep.svelte index 9c75ca1336..7a13353227 100644 --- a/frontend/src/lib/components/FlowGraphViewerStep.svelte +++ b/frontend/src/lib/components/FlowGraphViewerStep.svelte @@ -137,6 +137,8 @@ Inline {stepDetail.value.language} script {:else if stepDetail.value.type == 'script'} Workspace script + {:else if stepDetail.value.type == 'aiagent'} + AI Agent {/if} @@ -217,6 +219,11 @@ {:else} {/if} + {:else if stepDetail.value.type == 'aiagent'} +
+

Step Inputs

+ +
{:else if stepDetail.value.type == 'forloopflow'}

Iterator expression:

diff --git a/frontend/src/lib/components/FlowJobResult.svelte b/frontend/src/lib/components/FlowJobResult.svelte index d806b2fd07..7c3cf9a28a 100644 --- a/frontend/src/lib/components/FlowJobResult.svelte +++ b/frontend/src/lib/components/FlowJobResult.svelte @@ -2,6 +2,8 @@ import { Loader2 } from 'lucide-svelte' import DisplayResult from './DisplayResult.svelte' import LogViewer from './LogViewer.svelte' + import type { CompletedJob, FlowModule, Job } from '$lib/gen' + import AiAgentLogViewer from './AIAgentLogViewer.svelte' interface Props { waitingForExecutor?: boolean @@ -18,6 +20,12 @@ refreshLog?: boolean downloadLogs?: boolean tagLabel?: string | undefined + aiAgentStatus?: { + tools: FlowModule[] + agentJob: Partial & Pick & { type: 'CompletedJob' } + storedToolCallJobs?: Record + onToolJobLoaded?: (job: Job, idx: number) => void + } } let { @@ -33,7 +41,8 @@ tag = undefined, workspaceId = undefined, downloadLogs = true, - tagLabel = undefined + tagLabel = undefined, + aiAgentStatus = undefined }: Props = $props() @@ -54,13 +63,17 @@ {/if}
- + {#if aiAgentStatus} + + {:else} + + {/if}
diff --git a/frontend/src/lib/components/FlowLogViewer.svelte b/frontend/src/lib/components/FlowLogViewer.svelte index 369578ff17..fafe7eace3 100644 --- a/frontend/src/lib/components/FlowLogViewer.svelte +++ b/frontend/src/lib/components/FlowLogViewer.svelte @@ -21,13 +21,12 @@ import FlowJobsMenu from './flows/map/FlowJobsMenu.svelte' import BarsStaggered from './icons/BarsStaggered.svelte' import type { GraphModuleState } from './graph/model' - import type { Writable } from 'svelte/store' type RootJobData = Partial interface Props { modules: FlowModule[] - localModuleStates: Writable> + localModuleStates: Record rootJob: RootJobData flowStatus: FlowStatusModule['type'] | undefined expandedRows: Record @@ -46,6 +45,7 @@ ) => Promise getSelectedIteration: (stepId: string) => number flowSummary?: string + mode?: 'flow' | 'aiagent' } let { @@ -64,7 +64,8 @@ flowId = 'root', onSelectedIteration, getSelectedIteration, - flowSummary + flowSummary, + mode = 'flow' }: Props = $props() function getJobLink(jobId: string | undefined): string { @@ -97,16 +98,18 @@ function getStepProgress(job: RootJobData, totalSteps: number): string { if (totalSteps === 0) return '' + const stepWord = mode === 'aiagent' ? 'action' : 'step' + // If flow is completed, show total steps if (job.type === 'CompletedJob') { - return ` (${totalSteps} step${totalSteps === 1 ? '' : 's'})` + return ` (${totalSteps} ${stepWord}${totalSteps === 1 ? '' : 's'})` } // If flow is running, use flow_status.step if available (like JobStatus.svelte) if (job.type === 'QueuedJob') { if (job.flow_status?.step !== undefined) { const currentStep = (job.flow_status.step ?? 0) + 1 - return ` (step ${currentStep} of ${totalSteps})` + return ` (${stepWord} ${currentStep} of ${totalSteps})` } return '' @@ -122,7 +125,7 @@ } function hasEmptySubflow(stepId: string, stepType: FlowModuleValue['type'] | undefined): boolean { - const state = $localModuleStates[stepId] + const state = localModuleStates[stepId] if (!state || !stepType) return false return ( @@ -172,7 +175,7 @@ } // Check if this entry itself has an error (but don't flag it - only its parents) - const stepStatus = $localModuleStates[module.id]?.type + const stepStatus = localModuleStates[module.id]?.type if (stepStatus === 'Failure') { currentEntryHasError = true // Don't add the entry itself to parentsWithErrors @@ -318,7 +321,7 @@
- {level == 0 ? 'Flow' : 'Subflow'} + {mode === 'aiagent' ? 'AI Agent' : level == 0 ? 'Flow' : 'Subflow'} {#if flowInfo.label} : {flowInfo.label} {/if} @@ -402,7 +405,7 @@ {#if modules.length > 0} {#each modules as module (module.id)} {@const isLeafStep = !hasSubflows(module)} - {@const status = $localModuleStates[module.id]?.type} + {@const status = localModuleStates[module.id]?.type} {@const isRunning = status === 'InProgress' || status === 'WaitingForExecutor'} {@const hasEmptySubflowValue = hasEmptySubflow(module.id, module.value.type)} {@const isCollapsible = !hasEmptySubflowValue} @@ -451,20 +454,26 @@
- {module.id} + {mode === 'aiagent' + ? module.summary + ? 'Tool call' + : 'Message' + : module.id} - {#if module.value.type === 'forloopflow'} - For loop - {:else if module.value.type === 'whileloopflow'} - While loop - {:else if module.value.type === 'branchall'} - Branch to all - {:else if module.value.type === 'branchone'} - Branch to one - {:else if module.value.type === 'flow'} - Subflow - {:else} - Step + {#if mode === 'flow'} + {#if module.value.type === 'forloopflow'} + For loop + {:else if module.value.type === 'whileloopflow'} + While loop + {:else if module.value.type === 'branchall'} + Branch to all + {:else if module.value.type === 'branchone'} + Branch to one + {:else if module.value.type === 'flow'} + Subflow + {:else} + Step + {/if} {/if} {#if module.summary} : {module.summary} @@ -479,7 +488,7 @@ {/if} - {#if !hasEmptySubflowValue && $localModuleStates[module.id]?.flow_jobs && (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow')} + {#if !hasEmptySubflowValue && localModuleStates[module.id]?.flow_jobs && (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow')} @@ -488,18 +497,18 @@ moduleId={module.id} id={module.id} {onSelectedIteration} - flowJobsSuccess={$localModuleStates[module.id] + flowJobsSuccess={localModuleStates[module.id] ?.flow_jobs_success} - flowJobs={$localModuleStates[module.id]?.flow_jobs} - selected={$localModuleStates[module.id]?.selectedForloopIndex ?? + flowJobs={localModuleStates[module.id]?.flow_jobs} + selected={localModuleStates[module.id]?.selectedForloopIndex ?? 0} - selectedManually={$localModuleStates[module.id] + selectedManually={localModuleStates[module.id] ?.selectedForLoopSetManually ?? false} showIcon={false} /> {#if module.value.type === 'forloopflow'} - {`/${$localModuleStates[module.id]?.iteration_total ?? 0}`} + {`/${localModuleStates[module.id]?.iteration_total ?? 0}`} {/if} {/if} @@ -507,37 +516,39 @@
{#if isLeafStep} - {@const jobId = $localModuleStates[module.id]?.job_id} - - {truncateRev(jobId ?? '', 6)} - + {@const jobId = localModuleStates[module.id]?.job_id} + {#if jobId} + + {truncateRev(jobId ?? '', 6)} + + {/if} {/if}
{#if isCollapsible && isExpanded(module.id, isRunning)} - {@const args = $localModuleStates[module.id]?.args} - {@const logs = $localModuleStates[module.id]?.logs} - {@const result = $localModuleStates[module.id]?.result} - {@const jobId = $localModuleStates[module.id]?.job_id} + {@const args = localModuleStates[module.id]?.args} + {@const logs = localModuleStates[module.id]?.logs} + {@const result = localModuleStates[module.id]?.result} + {@const jobId = localModuleStates[module.id]?.job_id}
{#each getSubflows(module) as subflow} {@const subflowJob = { id: jobId, type: - $localModuleStates[module.id]?.type === 'Failure' || - $localModuleStates[module.id]?.type === 'Success' + localModuleStates[module.id]?.type === 'Failure' || + localModuleStates[module.id]?.type === 'Success' ? 'CompletedJob' : ('QueuedJob' as Job['type']), logs, result, args, - success: $localModuleStates[module.id]?.type === 'Success' + success: localModuleStates[module.id]?.type === 'Success' }}
@@ -545,7 +556,7 @@ modules={subflow.modules} {localModuleStates} rootJob={subflowJob} - flowStatus={$localModuleStates[module.id]?.type} + flowStatus={localModuleStates[module.id]?.type} {expandedRows} {allExpanded} {showResultsInputs} diff --git a/frontend/src/lib/components/FlowLogViewerWrapper.svelte b/frontend/src/lib/components/FlowLogViewerWrapper.svelte index 3d5c09f460..08bd19ae52 100644 --- a/frontend/src/lib/components/FlowLogViewerWrapper.svelte +++ b/frontend/src/lib/components/FlowLogViewerWrapper.svelte @@ -1,6 +1,5 @@ { - let { job } = detail + onJobsLoaded={({ job, force }) => { if (job.script_path != lastScriptPath && job.script_path) { lastScriptPath = job.script_path loadOwner(lastScriptPath ?? '') } - dispatch('jobsLoaded', job) + onJobsLoaded?.({ job, force }) }} globalModuleStates={[]} - globalDurationStatuses={[]} - {localModuleStates} - {localDurationStatuses} + {globalIterationBounds} + bind:localModuleStates bind:selectedNode={selectedJobStep} - on:start - on:done + bind:localDurationStatuses + {onStart} + {onDone} bind:job {initialJob} {jobId} @@ -122,4 +139,6 @@ {customUi} graphTabOpen={true} isNodeSelected={true} + {refreshGlobal} + {updateGlobalRefresh} /> diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index 6800d7583d..5d00608245 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -8,18 +8,24 @@ type FlowStatus, type FlowModuleValue, type FlowModule, - ResourceService + ResourceService, + type CompletedJob } from '$lib/gen' import { workspaceStore } from '$lib/stores' import { base } from '$lib/base' import FlowJobResult from './FlowJobResult.svelte' import DisplayResult from './DisplayResult.svelte' - import { createEventDispatcher, getContext, setContext, tick, untrack } from 'svelte' + import { getContext, setContext, tick, untrack } from 'svelte' import { onDestroy } from 'svelte' import { Badge, Button, Skeleton, Tab } from './common' import Tabs from './common/tabs/Tabs.svelte' - import { type DurationStatus, type FlowStatusViewerContext, type GraphModuleState } from './graph' + import { + type DurationStatus, + type FlowStatusViewerContext, + type GlobalIterationBounds, + type GraphModuleState + } from './graph' import ModuleStatus from './ModuleStatus.svelte' import { clone, isScriptPreview, msToSec, readFieldsRecursively, truncateRev } from '$lib/utils' import JobArgs from './JobArgs.svelte' @@ -27,7 +33,6 @@ import { deepEqual } from 'fast-equals' import FlowTimeline from './FlowTimeline.svelte' import { dfs } from './flows/dfs' - import { get, writable, type Unsubscriber, type Writable } from 'svelte/store' import Alert from './common/alert/Alert.svelte' import FlowGraphViewerStep from './FlowGraphViewerStep.svelte' import FlowGraphV2 from './graph/FlowGraphV2.svelte' @@ -38,8 +43,12 @@ import type { FlowGraphAssetContext } from './flows/types' import { createState } from '$lib/svelte5Utils.svelte' import JobLoader from './JobLoader.svelte' - - const dispatch = createEventDispatcher() + import { writable } from 'svelte/store' + import { + AI_TOOL_CALL_PREFIX, + AI_TOOL_MESSAGE_PREFIX, + getToolCallId + } from './graph/renderers/nodes/AIToolNode.svelte' let { flowStateStore, @@ -67,27 +76,30 @@ | undefined //only useful when forloops are optimized and the job doesn't contain the mod id anymore innerModule?: FlowModuleValue | undefined - globalRefreshes?: Record Promise> render?: boolean isOwner?: boolean selectedNode?: string | undefined - globalModuleStates: Writable>[] - globalDurationStatuses: Writable>[] - childFlow?: boolean + globalModuleStates: Record[] + globalDurationStatuses?: Record[] + globalIterationBounds: Record + isSelectedBranch?: boolean isSubflow?: boolean reducedPolling?: boolean wideResults?: boolean hideFlowResult?: boolean + topModuleStates?: Record workspace?: string | undefined prefix?: string | undefined - subflowParentsGlobalModuleStates?: Writable>[] - subflowParentsDurationStatuses?: Writable>[] + subflowParentsGlobalModuleStates?: Record[] + subflowParentsDurationStatuses?: Record[] isForloopSelected?: boolean - parentRecursiveRefresh?: Record Promise> + updateRecursiveRefreshFn?: (jobId: string, updateFn: (clear, root) => Promise) => void + refreshGlobal: (moduleId: string, clear: boolean, root: string) => Promise + updateGlobalRefresh: (moduleId: string, updateFn: (clear, root) => Promise) => void job?: (Job & { result_stream?: string }) | undefined rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states' - localModuleStates?: Writable> - localDurationStatuses?: Writable> + localModuleStates?: Record + localDurationStatuses?: Record onResultStreamUpdate?: ({ jobId, result_stream @@ -101,6 +113,9 @@ graphTabOpen: boolean isNodeSelected: boolean loadExtraLogs?: (logs: string) => void + onStart?: () => void + onJobsLoaded?: ({ job, force }: { job: Job; force: boolean }) => void + onDone?: ({ job }: { job: CompletedJob }) => void } let { @@ -109,34 +124,42 @@ workspaceId = undefined, flowJobIds = undefined, innerModule = undefined, - globalRefreshes = $bindable({}), render = true, isOwner = false, selectedNode = $bindable(undefined), globalModuleStates, - globalDurationStatuses, - childFlow = false, + globalDurationStatuses = [], + globalIterationBounds, + updateRecursiveRefreshFn = undefined, + isSelectedBranch = true, isSubflow = false, reducedPolling = false, wideResults = false, hideFlowResult = false, workspace = $workspaceStore, prefix = undefined, + topModuleStates = undefined, + refreshGlobal, + updateGlobalRefresh, subflowParentsGlobalModuleStates = [], subflowParentsDurationStatuses = [], isForloopSelected = false, - parentRecursiveRefresh = $bindable({}), job = $bindable(undefined), rightColumnSelect = $bindable('timeline'), - localModuleStates = writable({}), - localDurationStatuses = writable({}), + localModuleStates = $bindable({}), + localDurationStatuses = $bindable({}), customUi, onResultStreamUpdate = undefined, graphTabOpen, isNodeSelected, - loadExtraLogs = undefined + loadExtraLogs = undefined, + onStart = undefined, + onJobsLoaded = undefined, + onDone = undefined }: Props = $props() + let getTopModuleStates = $derived(topModuleStates ?? localModuleStates) + let resultStreams: Record = $state({}) if (onResultStreamUpdate == undefined) { @@ -152,6 +175,14 @@ } let recursiveRefresh: Record Promise> = $state({}) + let updateRecursiveRefreshInner = ( + childJobId: string, + updateFn: (clear, root) => Promise + ) => { + if (childJobId) { + recursiveRefresh[childJobId] = updateFn + } + } // Add support for the input args assets shown as an asset node const _flowGraphAssetsCtx = getContext('FlowGraphAssetContext') @@ -190,81 +221,79 @@ let expandedSubflows: Record = $state({}) - let selectedId: Writable = writable(selectedNode) + let selectedId = writable(selectedNode) function onFlowModuleId() { - if (globalRefreshes) { - let modId = flowJobIds?.moduleId - if (modId) { - globalRefreshes[buildSubflowKey(modId, prefix)] = async (clear, root) => { - await refresh(clear, root) // refresh(true, loopJob) - } - } + let modId = flowJobIds?.moduleId + if (modId) { + let prefixedId = buildSubflowKey(modId, prefix) + updateGlobalRefresh(prefixedId, async (clear, root) => { + // console.debug('updateGlobalRefreshInner refresh', prefixedId, clear, root) + await refresh(clear, root) // refresh(true, loopJob) + }) } } function updateModuleStates( - moduleState: Writable>, + moduleState: Record, key: string, newValue: GraphModuleState, keepType: boolean | undefined ) { - const state = get(moduleState) if ( newValue.selectedForloop != undefined && - state[key]?.selectedForloop != undefined && - newValue.selectedForloop != state[key].selectedForloop + moduleState[key]?.selectedForloop != undefined && + newValue.selectedForloop != moduleState[key].selectedForloop ) { + let newState = { ...moduleState[key] } if ( newValue.type == 'InProgress' && - state[key]?.type != 'InProgress' && - !(keepType && (state[key]?.type === 'Success' || state[key]?.type === 'Failure')) + moduleState[key]?.type != 'InProgress' && + !( + keepType && + (moduleState[key]?.type === 'Success' || moduleState[key]?.type === 'Failure') + ) ) { - moduleState.update((state) => { - state[key].type = 'InProgress' - return state - }) + newState.type = 'InProgress' + } else if (['Success', 'Failure'].includes(newValue.type)) { + newState.type = newValue.type } if ( - state[key]?.job_id != newValue.job_id || - !deepEqual(state[key]?.args, newValue.args) || - !deepEqual(state[key]?.result, newValue.result) + moduleState[key]?.job_id != newValue.job_id || + !deepEqual(moduleState[key]?.args, newValue.args) || + !deepEqual(moduleState[key]?.result, newValue.result) ) { - moduleState.update((state) => { - state[key].args = newValue.args - state[key].result = newValue.result - state[key].job_id = newValue.job_id - return state - }) + newState.args = newValue.args + newState.result = newValue.result + newState.job_id = newValue.job_id } + moduleState[key] = newState return } - if (state[key]?.selectedForLoopSetManually) { + if (moduleState[key]?.selectedForLoopSetManually) { if ( newValue.selectedForloop != undefined && - state[key]?.selectedForloop != newValue.selectedForloop + moduleState[key]?.selectedForloop != newValue.selectedForloop ) { - return state + return moduleState } else { newValue.selectedForLoopSetManually = true - newValue.selectedForloopIndex = state[key]?.selectedForloopIndex - newValue.selectedForloop = state[key]?.selectedForloop + newValue.selectedForloopIndex = moduleState[key]?.selectedForloopIndex + newValue.selectedForloop = moduleState[key]?.selectedForloop } - } else if (state[key]?.selectedForloopIndex != undefined) { - newValue.selectedForloopIndex = state[key]?.selectedForloopIndex - newValue.selectedForloop = state[key]?.selectedForloop + } else if (moduleState[key]?.selectedForloopIndex != undefined) { + newValue.selectedForloopIndex = moduleState[key]?.selectedForloopIndex + newValue.selectedForloop = moduleState[key]?.selectedForloop } - if (keepType && (state[key]?.type == 'Success' || state[key]?.type == 'Failure')) { - newValue.type = state[key].type + if (keepType && (moduleState[key]?.type == 'Success' || moduleState[key]?.type == 'Failure')) { + newValue.type = moduleState[key].type } - if (!deepEqual(state[key], newValue)) { - moduleState.update((state) => { - state[key] = newValue - return state - }) + if (!deepEqual(moduleState[key], newValue)) { + // console.debug('updateModuleStates 2', key, $state.snapshot(moduleState)) + moduleState[key] = newValue } } @@ -273,69 +302,72 @@ } async function refresh(clearLoop: boolean, rootJob: string | undefined) { + console.debug('refresh', clearLoop, rootJob) let modId = flowJobIds?.moduleId + let topModuleStates = getTopModuleStates if (clearLoop) { if (!rootJob) { - let topLevelModuleStates = globalModuleStates?.[globalModuleStates?.length - 1] - if (modId) { - topLevelModuleStates?.update((x) => { - if (modId) { - delete x[modId] - } - return x - }) + if (modId && topModuleStates) { + let prefixedId = buildSubflowKey(modId, prefix) + delete topModuleStates[prefixedId] // TODO: this is not working } if (subflowParentsGlobalModuleStates.length > 0) { - subflowParentsGlobalModuleStates?.[subflowParentsGlobalModuleStates?.length - 1]?.update( - (x) => { - for (let mod of innerModules ?? []) { - if (mod.id) { - delete x[buildSubflowKey(mod.id, prefix)] - } - } + let subflowModuleStates = + subflowParentsGlobalModuleStates?.[subflowParentsGlobalModuleStates?.length - 1] - return x + for (let mod of innerModules ?? []) { + if (mod.id) { + delete subflowModuleStates[buildSubflowKey(mod.id, prefix)] } - ) + } } else { - topLevelModuleStates?.update((x) => { - for (let mod of innerModules ?? []) { - if (mod.id) { - delete x[mod.id] - } + for (let mod of innerModules ?? []) { + if (mod.id && topModuleStates) { + let prefixedId = buildSubflowKey(mod.id, prefix) + delete topModuleStates[prefixedId] } - - return x - }) + } } } } else { - let state = modId ? getTopModuleStates()?.[modId] : undefined + let state = modId ? topModuleStates?.[buildSubflowKey(modId, prefix)] : undefined let loopjob = state?.selectedForloop let njob = flowJobIds && modId && loopjob ? storedListJobs?.[loopjob] : job + if (njob) { - dispatch('jobsLoaded', { job: njob, force: true }) + onJobsLoaded?.({ job: njob, force: true }) } } - for (let [k, rec] of Object.entries(recursiveRefresh)) { - if (rootJob != undefined && rootJob != k) { - continue - } + let callRec = async (rec: (clear, root) => Promise) => { await tick() await rec(clearLoop, undefined) } + if (rootJob) { + let rec = recursiveRefresh[rootJob] + if (rec) { + await callRec(rec) + // console.debug('refresh recursive 1', rec) + } else { + // console.debug('refresh recursive no rec', rootJob) + } + } else { + for (let rec of Object.values(recursiveRefresh)) { + await callRec(rec) + // console.debug('refresh recursive 2', rec) + } + } } function updateRecursiveRefresh(jobId: string) { if (jobId) { - parentRecursiveRefresh[jobId] = async (clear, root) => { + updateRecursiveRefreshFn?.(jobId, async (clear, root) => { if (globalModuleStates.length > 0 || isSubflow) { await refresh(clear, root) } - } + }) } } @@ -345,67 +377,55 @@ force?: boolean, keepType?: boolean ) { - let newValue = { ...($localModuleStates[key] ?? {}), ...value } - if (!deepEqual($localModuleStates[key], value) || force) { + let newValue = { ...(localModuleStates[key] ?? {}), ...value } + if (!deepEqual(localModuleStates[key], value) || force) { + // console.debug('setModuleState', key, force, keepType, $state.snapshot(value)) ;[localModuleStates, ...globalModuleStates].forEach((s) => { updateModuleStates(s, key, newValue, keepType) }) if (prefix) { - subflowParentsGlobalModuleStates.forEach((s) => - updateModuleStates(s, buildSubflowKey(key, prefix), newValue, keepType) - ) + let prefixedId = buildSubflowKey(key, prefix) + subflowParentsGlobalModuleStates.forEach((s) => { + updateModuleStates(s, prefixedId, newValue, keepType) + }) } } } function setDurationStatusByJob(key: string, id: string, value: any) { - if (!deepEqual($localDurationStatuses[key]?.byJob[id], value)) { - $localDurationStatuses[key].byJob[id] = value + if (!deepEqual(localDurationStatuses[key]?.byJob[id], value)) { + localDurationStatuses[key].byJob[id] = value globalDurationStatuses.forEach((s) => { - s.update((x) => { - x[key].byJob[id] = value - - return x - }) + s[key].byJob[id] = value }) if (prefix) { subflowParentsDurationStatuses.forEach((s) => { - s.update((x) => { - x[buildSubflowKey(key, prefix)].byJob[id] = value - return x - }) + s[buildSubflowKey(key, prefix)].byJob[id] = value }) } } } function initializeByJob(modId: string) { - if ($localDurationStatuses[modId] == undefined) { - $localDurationStatuses[modId] = { byJob: {} } + if (localDurationStatuses[modId] == undefined) { + localDurationStatuses[modId] = { byJob: {} } } - globalDurationStatuses.forEach((x) => - x.update((x) => { - if (x[modId] == undefined) { - x[modId] = { byJob: {} } - } - return x - }) - ) + globalDurationStatuses.forEach((x) => { + if (x[modId] == undefined) { + x[modId] = { byJob: {} } + } + }) if (prefix) { - subflowParentsDurationStatuses.forEach((x) => - x.update((x) => { - let key = buildSubflowKey(modId, prefix) - if (x[key] == undefined) { - x[key] = { byJob: {} } - } - return x - }) - ) + subflowParentsDurationStatuses.forEach((x) => { + let key = buildSubflowKey(modId, prefix) + if (x[key] == undefined) { + x[key] = { byJob: {} } + } + }) } } - let innerModules: FlowStatusModule[] = $state([]) - + let innerModules = $state(undefined) as FlowStatusModule[] | undefined function updateStatus(status: FlowStatus) { innerModules = status?.modules?.concat( @@ -418,29 +438,27 @@ let count = status.retry?.fail_count if (count) { - $retryStatus[jobId ?? ''] = count - } else if ($retryStatus[jobId ?? ''] != undefined) { - delete $retryStatus[jobId ?? ''] - $retryStatus = $retryStatus + retryStatus.val[jobId ?? ''] = count + } else if (retryStatus.val[jobId ?? ''] != undefined) { + delete retryStatus.val[jobId ?? ''] } let jobStatus = job?.flow_status?.modules?.[job?.flow_status.step] if (jobStatus && jobStatus.count != undefined) { - $suspendStatus[jobId ?? ''] = { nb: jobStatus.count, job: job! } - } else if ($suspendStatus[jobId ?? ''] != undefined) { - delete $suspendStatus[jobId ?? ''] - $suspendStatus = $suspendStatus + suspendStatus.val[jobId ?? ''] = { nb: jobStatus.count, job: job! } + } else if (suspendStatus.val[jobId ?? ''] != undefined) { + delete suspendStatus.val[jobId ?? ''] } } function updateInnerModules() { - if ($localModuleStates) { - innerModules.forEach((mod, i) => { + if (localModuleStates) { + innerModules?.forEach((mod, i) => { if (mod.type === 'WaitingForEvents' && innerModules?.[i - 1]?.type === 'Success') { setModuleState(mod.id ?? '', { type: mod.type, args: job?.args, tag: job?.tag }) } else if ( mod.type === 'WaitingForExecutor' && - $localModuleStates[mod.id ?? '']?.scheduled_for == undefined + localModuleStates[mod.id ?? '']?.scheduled_for == undefined ) { JobService.getJob({ workspace: workspaceId ?? $workspaceStore ?? '', @@ -466,7 +484,7 @@ } else if ( (mod.flow_jobs || mod.branch_chosen) && (mod.type == 'Success' || mod.type == 'Failure') && - !['Success', 'Failure'].includes($localModuleStates?.[mod.id ?? '']?.type) + !['Success', 'Failure'].includes(localModuleStates?.[mod.id ?? '']?.type) ) { let branchChosen = mod.branch_chosen ? { @@ -474,6 +492,7 @@ mod.branch_chosen.type == 'default' ? 0 : (mod.branch_chosen.branch ?? 0) + 1 } : {} + console.debug('updateInnerModules', mod.id, mod.type, branchChosen) setModuleState( mod.id ?? '', { @@ -486,9 +505,34 @@ setModuleState(mod.id ?? '', {}, true) } - if (mod.flow_jobs_success) { + if (mod.flow_jobs_success || mod.flow_jobs) { setModuleState(mod.id ?? '', { - flow_jobs_success: mod.flow_jobs_success + flow_jobs_success: mod.flow_jobs_success, + flow_jobs: mod.flow_jobs, + iteration_total: mod.iterator?.itered?.length ?? mod.flow_jobs?.length + }) + } + + if (mod.agent_actions && mod.id) { + setModuleState(mod.id, { + agent_actions: mod.agent_actions + }) + mod.agent_actions.forEach((action, idx) => { + if (mod.id) { + if (action.type == 'tool_call') { + const toolCallId = getToolCallId(idx, mod.id, action.module_id) + const success = mod.agent_actions_success?.[idx] + setModuleState(toolCallId, { + job_id: action.job_id, + type: success != undefined ? (success ? 'Success' : 'Failure') : 'InProgress' + }) + } else if (action.type == 'message') { + const toolCallId = getToolCallId(idx, mod.id) + setModuleState(toolCallId, { + type: 'Success' + }) + } + } }) } }) @@ -519,20 +563,20 @@ let jobLoader: JobLoader | undefined = undefined function setJob(newJob: Job, force: boolean) { - if (!deepEqual(job, newJob) || isForloopSelected || force) { + if (!deepEqual(job, newJob) || isForloopSelected || force || innerModules == undefined) { job = newJob job?.flow_status && updateStatus(job?.flow_status) - dispatch('jobsLoaded', { job, force: false }) + onJobsLoaded?.({ job, force: false }) notAnonynmous = false if (job?.type == 'CompletedJob' && !destroyed) { - dispatch('done', job) + onDone?.({ job }) } } } async function loadJobInProgress() { if (!started) { started = true - dispatch('start') + onStart?.() } if (jobId != '00000000-0000-0000-0000-000000000000') { try { @@ -577,68 +621,46 @@ updateRecursiveRefresh(jobId) async function updateJobId() { - if (jobId !== job?.id) { - $localModuleStates = {} + if (jobId !== job?.id || innerModules == undefined) { + localModuleStates = {} flowTimeline?.reset() timeout && clearTimeout(timeout) - innerModules = [] + innerModules = undefined + console.log('updateJobId', jobId) if (flowJobIds) { let modId = flowJobIds?.moduleId ?? '' - let common = { + if (localDurationStatuses[modId] == undefined) { + localDurationStatuses[modId] = { byJob: {} } + } + let prefixed = buildSubflowKey(modId, prefix) + globalDurationStatuses.forEach((x) => { + if (x[prefixed] == undefined) { + x[prefixed] = { byJob: {} } + } + }) + + globalIterationBounds[prefixed] = { iteration_from: flowJobIds?.branchall ? 0 : Math.max(flowJobIds.flowJobs.length - 20, 0), - iteration_total: $localDurationStatuses?.[modId]?.iteration_total ?? flowJobIds?.length + iteration_total: flowJobIds?.length } - $localDurationStatuses[modId] = { - ...($localDurationStatuses[modId] ?? { byJob: {} }), - ...common - } - let prefixed = modId - globalDurationStatuses.forEach((x) => - x.update((x) => { - x[prefixed] = { ...(x[prefixed] ?? { byJob: {} }), ...common } - return x - }) - ) } else { - updateRecursiveRefresh(jobId) recursiveRefresh = {} - $localDurationStatuses = {} + localDurationStatuses = {} + updateRecursiveRefresh(jobId) } await loadJobInProgress() } } - function getTopModuleStates() { - return get(globalModuleStates?.[globalModuleStates?.length - 1]) - } - - let forloop_selected = $state(getTopModuleStates()?.[flowJobIds?.moduleId ?? '']?.selectedForloop) - - let sub: Unsubscriber | undefined = undefined - let timeoutForloopSelectedSub: NodeJS.Timeout | undefined = undefined - let timeoutForloopSelected: NodeJS.Timeout | undefined = undefined - - function onModuleIdChange() { - clearTimeout(timeoutForloopSelectedSub) - timeoutForloopSelectedSub = setTimeout(() => { - sub?.() - sub = globalModuleStates?.[globalModuleStates?.length - 1].subscribe((x) => { - const newForloopSelected = x[flowJobIds?.moduleId ?? '']?.selectedForloop - if (newForloopSelected != forloop_selected) { - clearTimeout(timeoutForloopSelected) - timeoutForloopSelected = setTimeout(() => { - forloop_selected = newForloopSelected - }, 200) - } - }) - }, 200) - } + let forloop_selected = $state( + getTopModuleStates?.[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)]?.selectedForloop + ) onDestroy(() => { destroyed = true timeout && clearTimeout(timeout) - sub?.() + // sub?.() }) function isSuccess(arg: any): boolean | undefined { @@ -649,25 +671,25 @@ } } - function onJobsLoaded(mod: FlowStatusModule, job: Job, force?: boolean): void { - if (mod.id && (mod.flow_jobs ?? []).length == 0) { - if (!childFlow) { - if ($flowStateStore?.[mod.id]) { - $flowStateStore[mod.id] = { - ...$flowStateStore[mod.id], - previewResult: job['result'], - previewArgs: job.args, - previewJobId: job.id, - previewWorkspaceId: job.workspace_id, - previewSuccess: job['success'] - } + function onJobsLoadedInner(mod: FlowStatusModule, job: Job, force?: boolean): void { + let id = mod.id + if (id && ((mod.flow_jobs ?? []).length == 0 || force)) { + // console.debug('onJobsLoadedInner', id, job.id, force) + if (flowStateStore) { + flowStateStore[buildSubflowKey(id, prefix)] = { + ...(flowStateStore?.[buildSubflowKey(id, prefix)] ?? {}), + previewResult: job['result'], + previewArgs: job.args, + previewJobId: job.id, + previewSuccess: job['success'] } } - initializeByJob(mod.id) + + initializeByJob(id) let started_at = job.started_at ? new Date(job.started_at).getTime() : undefined if (job.type == 'QueuedJob') { setModuleState( - mod.id, + id, { type: 'InProgress', job_id: job.id, @@ -679,7 +701,7 @@ }, force ) - setDurationStatusByJob(mod.id, job.id, { + setDurationStatusByJob(id, job.id, { created_at: job.created_at ? new Date(job.created_at).getTime() : undefined, started_at }) @@ -687,10 +709,10 @@ const parent_module = mod['parent_module'] // Delete existing failure node attached to the same parent module - removeFailureNode(mod.id, parent_module) + removeFailureNode(id, parent_module) setModuleState( - mod.id, + id, { args: job.args, type: job['success'] ? 'Success' : 'Failure', @@ -705,13 +727,14 @@ flow_jobs_success: mod.flow_jobs_success, iteration_total: mod.iterator?.itered?.length, retries: mod?.failed_retries?.length, - skipped: mod.skipped - // retries: $flowStateStore?.raw_flow + skipped: mod.skipped, + agent_actions: mod.agent_actions + // retries: flowStateStore?.raw_flow }, force ) - setDurationStatusByJob(mod.id, job.id, { + setDurationStatusByJob(id, job.id, { created_at: job.created_at ? new Date(job.created_at).getTime() : undefined, started_at, duration_ms: job['duration_ms'] @@ -728,12 +751,13 @@ isForloop: boolean ) { if (modId) { - let globalState = globalModuleStates?.[globalModuleStates?.length - 1] - let globalStateGet = globalState ? get(globalState) : undefined - let state = globalStateGet?.[modId] + let prefixedId = buildSubflowKey(modId, prefix) + + let topModuleStates = getTopModuleStates + let state = topModuleStates?.[prefixedId] if (clicked && state?.selectedForloop) { - await globalRefreshes?.[modId]?.(true, state.selectedForloop) + await refreshGlobal?.(prefixedId, true, state.selectedForloop) } let manualOnce = state?.selectedForLoopSetManually if ( @@ -743,7 +767,7 @@ ) { let setManually = clicked || manualOnce - let newState = { + let newState: Partial = { ...(state ?? {}), selectedForloop: id, selectedForloopIndex: j, @@ -754,21 +778,20 @@ id != state?.selectedForloop || j != state?.selectedForloopIndex || setManually != state?.selectedForLoopSetManually + // console.debug('setIteration', selectedNotEqual, state, topModuleStates) if (selectedNotEqual) { - globalState?.update((topLevelModuleStates) => { - topLevelModuleStates[modId] = { + if (topModuleStates) { + topModuleStates[prefixedId] = { type: 'WaitingForPriorSteps', args: {}, ...newState } - return topLevelModuleStates - // clicked && callGlobRefresh(modId, {index: j, job: id, selectedManually: setManually ?? false}) - }) + } } } if (clicked) { - await globalRefreshes?.[modId]?.(false, id) + await refreshGlobal?.(prefixedId, false, id) } } } @@ -785,28 +808,33 @@ innerModule?.type == 'forloopflow' || innerModule?.type == 'whileloopflow' ) - if ($flowStateStore && $flowStateStore?.[modId] == undefined) { - $flowStateStore[modId] = { - ...(($flowStateStore[modId] as object) ?? {}), - previewResult: jobLoaded.args - } + let prefixedId = buildSubflowKey(modId, prefix) + + // if (flowStateStore) { + // flowStateStore[modId] = { + // ...((flowStateStore[modId] as object) ?? {}), + // previewResult: jobLoaded.args + // } + // } + if (flowStateStore && flowStateStore[prefixedId] == undefined) { + flowStateStore[prefixedId] = {} } - if ($flowStateStore?.[modId]) { - if (!childFlow) { - if ( - !$flowStateStore[modId].previewResult || - !Array.isArray($flowStateStore[modId]?.previewResult) - ) { - $flowStateStore[modId].previewResult = [] - } - $flowStateStore[modId].previewArgs = jobLoaded.args + if (flowStateStore) { + if ( + !flowStateStore?.[prefixedId]?.previewResult || + !Array.isArray(flowStateStore[prefixedId]?.previewResult) + ) { + flowStateStore[prefixedId].previewResult = [] } - if (jobLoaded.type == 'QueuedJob') { - jobResults[j] = 'Job in progress ...' - } else if (jobLoaded.type == 'CompletedJob') { - $flowStateStore[modId].previewResult[j] = jobLoaded.result - jobResults[j] = jobLoaded.result + flowStateStore[prefixedId].previewArgs = jobLoaded.args + } + if (jobLoaded.type == 'QueuedJob') { + jobResults[j] = 'Job in progress ...' + } else if (jobLoaded.type == 'CompletedJob') { + if (flowStateStore?.[prefixedId]) { + flowStateStore[prefixedId].previewResult[j] = jobLoaded.result } + jobResults[j] = jobLoaded.result } let started_at = jobLoaded.started_at ? new Date(jobLoaded.started_at).getTime() : undefined @@ -824,15 +852,15 @@ duration_ms: undefined } - let currentIndex = getTopModuleStates()?.[modId]?.selectedForloopIndex == j + let currentIndex = getTopModuleStates?.[prefixedId]?.selectedForloopIndex == j + if (currentIndex) { v.logs = jobLoaded.logs v.args = jobLoaded.args v.job_id = jobLoaded.id } - if (jobLoaded.type == 'QueuedJob') { - if (started_at && $localModuleStates[modId]?.type != 'InProgress') { + if (started_at && localModuleStates[modId]?.type != 'InProgress') { v.type = 'InProgress' } } else if (jobLoaded.type == 'CompletedJob') { @@ -842,7 +870,6 @@ } } setModuleState(modId, v, force, true) - if (jobLoaded.type == 'QueuedJob') { setDurationStatusByJob(modId, job_id, { created_at, @@ -865,7 +892,7 @@ id = innerModule?.modules?.[0]?.id } if (id) { - onJobsLoaded({ id } as FlowStatusModule, jobLoaded) + onJobsLoadedInner({ id } as FlowStatusModule, jobLoaded) } } } @@ -873,38 +900,35 @@ let flowTimeline: FlowTimeline | undefined = $state() - function loadPreviousIters(lenToAdd: number) { - let r = $localDurationStatuses[flowJobIds?.moduleId ?? ''] - if (r.iteration_from) { - r.iteration_from -= lenToAdd - $localDurationStatuses = $localDurationStatuses - globalDurationStatuses.forEach((x) => x.update((x) => x)) + function loadPreviousIters(innerKey: string, lenToAdd: number) { + let key = buildSubflowKey(innerKey, prefix) + if (globalIterationBounds[key]) { + globalIterationBounds[key].iteration_from = + (globalIterationBounds[key]?.iteration_from ?? 0) - lenToAdd } - jobResults = [ - ...[...new Array(lenToAdd).keys()].map((x) => 'not computed or loaded yet'), - ...jobResults - ] // updateSlicedListJobIds() } let stepDetail: FlowModule | string | undefined = $state(undefined) let storedListJobs: Record = $state({}) + + let storedToolCallJobs: Record = $state({}) + let selectedToolCall: number | undefined = $state(undefined) + let toolCallIndicesToLoad: number[] = $state([]) + let wrapperHeight: number = $state(0) function removeFailureNode(id: string, parent_module: any) { if (id?.startsWith('failure-') && parent_module) { - ;[...globalModuleStates, localModuleStates].forEach((stateMapStore) => { - stateMapStore.update((stateMap) => { - if (id) { - Object.keys(stateMap).forEach((key) => { - if (stateMap[key]?.parent_module == parent_module) { - delete stateMap[key] - } - }) - } - return stateMap - }) + ;[...globalModuleStates, localModuleStates].forEach((stateMap) => { + if (id) { + Object.keys(stateMap).forEach((key) => { + if (stateMap[key]?.parent_module == parent_module) { + delete stateMap[key] + } + }) + } }) } } @@ -913,7 +937,7 @@ modules: FlowModule[], expandedSubflows: Record ): string[] { - const ids = dfs(modules, (x) => x.id) + const ids = dfs(modules, (x) => x.id, { skipToolNodes: true }) function rec(ids: string[], prefix: string | undefined): string[] { return ids.concat( @@ -941,35 +965,49 @@ let subflowsSize = $state(500) + function setParentModuleState(modId: string, state: Partial) { + ;[localModuleStates, ...globalModuleStates].forEach((stateMap) => { + if (stateMap[modId]) { + stateMap[modId] = { ...stateMap[modId], ...state } + } + }) + if (prefix) { + let prefixedId = buildSubflowKey(modId, prefix) + subflowParentsGlobalModuleStates.forEach((stateMap) => { + if (stateMap[prefixedId]) { + stateMap[prefixedId] = { ...stateMap[prefixedId], ...state } + } + }) + } + } async function onSelectedIteration( detail: | { id: string; index: number; manuallySet: true; moduleId: string } | { manuallySet: false; moduleId: string } ) { + let prefixedId = buildSubflowKey(detail.moduleId, prefix) if (detail.manuallySet) { let rootJobId = detail.id await tick() - let previousId = $localModuleStates[detail.moduleId]?.selectedForloop + let previousId = getTopModuleStates?.[prefixedId]?.selectedForloop if (previousId) { - await globalRefreshes?.[detail.moduleId]?.(true, previousId) + await refreshGlobal?.(prefixedId, true, previousId) } - $localModuleStates[detail.moduleId] = { - ...$localModuleStates[detail.moduleId], + setParentModuleState(detail.moduleId, { selectedForloop: detail.id, selectedForloopIndex: detail.index, selectedForLoopSetManually: true - } + }) await tick() - await globalRefreshes?.[detail.moduleId]?.(false, rootJobId) + await refreshGlobal?.(prefixedId, false, rootJobId) } else { - $localModuleStates[detail.moduleId] = { - ...$localModuleStates[detail.moduleId], + setParentModuleState(detail.moduleId, { selectedForLoopSetManually: false - } + }) } } @@ -984,7 +1022,17 @@ }) let isListJob = $derived(flowJobIds != undefined && Array.isArray(flowJobIds?.flowJobs)) $effect(() => { - flowJobIds?.moduleId && untrack(() => onModuleIdChange()) + if (isSelectedBranch) { + let modId = flowJobIds?.moduleId + if (modId) { + let selectedForloop = getTopModuleStates?.[buildSubflowKey(modId, prefix)]?.selectedForloop + untrack(() => { + if (selectedForloop != forloop_selected) { + forloop_selected = selectedForloop + } + }) + } + } }) let selected = $derived(isListJob ? 'sequence' : 'graph') as 'sequence' | 'graph' | 'logs' @@ -1006,16 +1054,17 @@
{/if} --> {#if isListJob} - {@const sliceFrom = $localDurationStatuses[flowJobIds?.moduleId ?? '']?.iteration_from ?? 0} + {@const sliceFrom = + globalIterationBounds[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)] + ?.iteration_from ?? 0} {@const lenToAdd = Math.min(20, sliceFrom)} - {#if (flowJobIds?.flowJobs.length ?? 0) > 20 && lenToAdd > 0} {@const allToAdd = (flowJobIds?.length ?? 0) - sliceFrom}

For performance reasons, only the last 20 items are shown by default @@ -1024,7 +1073,7 @@ @@ -1058,7 +1107,7 @@

{/if} {#if render} - {#if innerModules.length > 0 && !isListJob} + {#if innerModules && innerModules.length > 0 && !isListJob} Graph {#if isListJob} - {@const sliceFrom = $localDurationStatuses[flowJobIds?.moduleId ?? '']?.iteration_from ?? 0} + {@const sliceFrom = + globalIterationBounds[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)] + ?.iteration_from ?? 0}

Subflows ({flowJobIds?.flowJobs.length})

@@ -1125,9 +1176,10 @@
20)} {workspaceId} jobId={loopJobId} - on:jobsLoaded={(e) => { - let { job, force } = e.detail + onJobsLoaded={({ job, force }) => { storedListJobs[j] = job innerJobLoaded(job, j, false, force) }} {onResultStreamUpdate} graphTabOpen={selected == 'graph' && graphTabOpen} isNodeSelected={forloop_selected == loopJobId} + {globalIterationBounds} />
{/if} {/each}
- {:else if innerModules.length > 0 && (job.raw_flow?.modules.length ?? 0) > 0} + {:else if innerModules && innerModules.length > 0 && (job.raw_flow?.modules.length ?? 0) > 0} {@const hasPreprocessor = innerModules[0]?.id == 'preprocessor' ? 1 : 0}