agentengine-sdk-python 0.8.2__py3-none-any.whl → 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/METADATA +18 -25
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +224 -169
- ksadk/_build_provenance.json +1 -0
- ksadk/agui/_agent_helpers.py +4 -0
- ksadk/api/client.py +3 -0
- ksadk/builders/managed_runtime_builder.py +3 -0
- ksadk/cli/__init__.py +6 -0
- ksadk/cli/cmd_plugin.py +657 -0
- ksadk/codex/runtime.py +50 -8
- ksadk/configs/env_registry.py +31 -3
- ksadk/configs/settings.py +9 -0
- ksadk/context_engine/contributors.py +73 -8
- ksadk/conversations/__init__.py +19 -0
- ksadk/conversations/context.py +104 -24
- ksadk/conversations/contracts.py +266 -0
- ksadk/conversations/message_projection.py +19 -1
- ksadk/conversations/projector.py +616 -0
- ksadk/conversations/reducer.py +98 -0
- ksadk/events/_v1_compat/projection.py +2 -0
- ksadk/events/adapters/codex.py +190 -0
- ksadk/harness/__init__.py +45 -35
- ksadk/harness/runtime.py +60 -1
- ksadk/kernel/contract_fingerprints.py +5 -4
- ksadk/kernel/contracts.py +8 -0
- ksadk/kernel/control.py +1 -0
- ksadk/kernel/ingress.py +21 -0
- ksadk/kernel/memory_store.py +1 -0
- ksadk/kernel/postgres_store.py +1 -0
- ksadk/kernel/sqlite_store.py +1 -0
- ksadk/kernel/worker.py +11 -0
- ksadk/memory/providers/local_sqlite.py +7 -0
- ksadk/plugins/__init__.py +167 -0
- ksadk/plugins/bridges/__init__.py +45 -0
- ksadk/plugins/bridges/codex.py +552 -0
- ksadk/plugins/bridges/dsh.py +1116 -0
- ksadk/plugins/builtins.py +747 -0
- ksadk/plugins/bundle.py +246 -0
- ksadk/plugins/bundle_security.py +167 -0
- ksadk/plugins/composition.py +529 -0
- ksadk/plugins/context_contributor.py +437 -0
- ksadk/plugins/contracts.py +451 -0
- ksadk/plugins/dsh_toolchain.py +935 -0
- ksadk/plugins/ecosystem_bridge.py +1105 -0
- ksadk/plugins/ecosystem_probe.py +191 -0
- ksadk/plugins/host.py +724 -0
- ksadk/plugins/providers/__init__.py +85 -0
- ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
- ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
- ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
- ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
- ksadk/plugins/providers/codex.py +722 -0
- ksadk/plugins/providers/codex_dsh.py +83 -0
- ksadk/plugins/providers/dsh.py +951 -0
- ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
- ksadk/plugins/providers/harness.py +627 -0
- ksadk/plugins/providers/harness_dsh.py +91 -0
- ksadk/plugins/providers/legacy.py +215 -0
- ksadk/plugins/providers/legacy_catalog.py +100 -0
- ksadk/plugins/providers/shipped_dsh.py +370 -0
- ksadk/plugins/resolver.py +331 -0
- ksadk/plugins/subagent_providers/__init__.py +12 -0
- ksadk/plugins/subagent_providers/codex.py +443 -0
- ksadk/plugins/subagents.py +333 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +51 -42
- ksadk/runtime/adapter.py +1 -0
- ksadk/runtime/factory.py +76 -2
- ksadk/runtime/framework_adapters.py +18 -0
- ksadk/runtime/runner_adapter.py +5 -0
- ksadk/sandbox/__init__.py +2 -1
- ksadk/sandbox/factory.py +72 -0
- ksadk/scheduler/__init__.py +18 -0
- ksadk/scheduler/calendar.py +102 -0
- ksadk/scheduler/contracts.py +254 -0
- ksadk/scheduler/dispatcher.py +129 -0
- ksadk/scheduler/engine.py +416 -0
- ksadk/scheduler/sqlite_store.py +563 -0
- ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-10GPXIJ6.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-BXk1l-G1.js} +1 -1
- ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-D9uB55Oh.js} +1 -1
- ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock--OEYoXIJ.js} +6 -6
- ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-DrEe1syq.js} +1 -1
- ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-eeTFJLvo.js} +1 -1
- ksadk/server/static/assets/{arc-C4FzinUA.js → arc-S3ZNbIAd.js} +1 -1
- ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-CRjgEJDX.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-C7bsBoNH.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-BX3c33Ob.js} +1 -1
- ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
- ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
- ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
- ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-CZwF3oUP.js} +1 -1
- ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
- ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-CnorGV5q.js} +1 -1
- ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CexntQA-.js} +1 -1
- ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-CHTC7UxA.js} +1 -1
- ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-BC1EWt_-.js} +1 -1
- ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-BTG24xN0.js} +1 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
- ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-DEOCBe15.js} +1 -1
- ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-DBThPuLa.js} +1 -1
- ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-ClTPrTht.js} +1 -1
- ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-DsZHrnpJ.js} +1 -1
- ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-BJR4nhVr.js} +1 -1
- ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-xzcERX0a.js} +1 -1
- ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-tn2xM1xu.js} +1 -1
- ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-BZPGBkw5.js} +1 -1
- ksadk/server/static/assets/{dist-ClxGviKw.js → dist-4zN1_tq5.js} +1 -1
- ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-8VWI-2bZ.js} +1 -1
- ksadk/server/static/assets/{dist-BX8z72IC.js → dist-B-46q3Hk.js} +1 -1
- ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-B0gdk4rl.js} +1 -1
- ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-B1oWRmrH.js} +1 -1
- ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-BI1qQH4n.js} +1 -1
- ksadk/server/static/assets/{dist-dydu68Fl.js → dist-BJLUwH6W.js} +1 -1
- ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-Bbwqa8TG.js} +1 -1
- ksadk/server/static/assets/{dist-CttYUqzS.js → dist-Btyo4vX4.js} +1 -1
- ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C71CsujO.js} +1 -1
- ksadk/server/static/assets/{dist-dDdADKFA.js → dist-CXpH_Rdd.js} +1 -1
- ksadk/server/static/assets/{dist-CSh_DE14.js → dist-Cq4OL6nO.js} +1 -1
- ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-D1V3f1B4.js} +1 -1
- ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-DJmCPRVB.js} +1 -1
- ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-DQAB0qn_.js} +1 -1
- ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DUwKa_l6.js} +1 -1
- ksadk/server/static/assets/{dist-B7seoj3d.js → dist-DdRBIceA.js} +1 -1
- ksadk/server/static/assets/{dist-BytlxIDT.js → dist-DfGFrb0v.js} +1 -1
- ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-DjhwGUgZ.js} +1 -1
- ksadk/server/static/assets/{dist-W-TIVntx.js → dist-DyqV4fL4.js} +1 -1
- ksadk/server/static/assets/{dist-DUX-id_F.js → dist-M67Hwu0n.js} +1 -1
- ksadk/server/static/assets/{dist-DmldrHd_.js → dist-u8tVH5Ak.js} +1 -1
- ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-DVfMsLge.js} +1 -1
- ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CWEZSYaw.js} +1 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-Dr5okd8V.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-VEO-cVcO.js +1 -0
- ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-TWIxoyYL.js} +1 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-DDh2hd2S.js} +1 -1
- ksadk/server/static/assets/index-B2k_urY8.js +241 -0
- ksadk/server/static/assets/index-ILPt6IWF.css +2 -0
- ksadk/server/static/assets/info-A6RAGUB7-ubA4Nm_p.js +1 -0
- ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CgoyWiDR.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CNP3ui66.js} +1 -1
- ksadk/server/static/assets/{katex-vFWytM5c.js → katex-DWEzQy1d.js} +1 -1
- ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-C-qHp7d2.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-Cl-K943T.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +1 -1
- ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
- ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-BQVc9ia_.js} +1 -1
- ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
- ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-B6UCAvtY.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +1 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-l5noef9X.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-Baea6ZvM.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-OKn8zZnU.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-DdKAKC4l.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-Bds8udlG.js +1 -0
- ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-Dt3FFDmK.js} +1 -1
- ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-Cz_vQtgF.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +1 -1
- ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DXX54iyO.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-Dp_lw9II.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
- ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-D8O5UlLG.js} +1 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
- ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +1 -1
- ksadk/server/static/assets/treeView-Q6P3EWNA-CK5CSrSO.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-Ch2BKgAM.js +1 -0
- ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-EnNl0DE7.js} +1 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-DBu5fbYr.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-BSVXs4IY.js} +1 -1
- ksadk/server/static/index.html +2 -2
- ksadk/skills/service_client.py +48 -0
- ksadk/studio/agent_lifecycle.py +26 -20
- ksadk/studio/api.py +513 -71
- ksadk/studio/api_catalog_routes.py +5 -0
- ksadk/studio/api_contracts.py +34 -1
- ksadk/studio/api_plugin_routes.py +480 -0
- ksadk/studio/attachment_store.py +187 -0
- ksadk/studio/authoring.py +10 -4
- ksadk/studio/authoring_coordinator.py +12 -2
- ksadk/studio/builder.py +123 -43
- ksadk/studio/cloud.py +4 -0
- ksadk/studio/codex_agent_service.py +12 -1
- ksadk/studio/codex_builder.py +54 -15
- ksadk/studio/codex_manifest.py +21 -1
- ksadk/studio/codex_run.py +16 -4
- ksadk/studio/compatibility_report.py +445 -0
- ksadk/studio/compiler.py +6 -1
- ksadk/studio/contracts.py +123 -4
- ksadk/studio/dsh_provider_registration.py +768 -0
- ksadk/studio/event_store.py +54 -6
- ksadk/studio/framework_run.py +37 -4
- ksadk/studio/hosted_kernel.py +1 -1
- ksadk/studio/operations.py +12 -2
- ksadk/studio/plugin_composition.py +301 -0
- ksadk/studio/plugin_kernel_adapter.py +128 -0
- ksadk/studio/plugin_runtime.py +652 -0
- ksadk/studio/resource_catalog.py +6 -1
- ksadk/studio/run_service.py +713 -75
- ksadk/studio/runtime_source.py +9 -1
- ksadk/studio/scheduler_runtime.py +374 -0
- ksadk/studio/scheduler_service.py +211 -0
- ksadk/studio/service.py +672 -82
- ksadk/studio/shared_web.py +167 -28
- ksadk/studio/soul.py +52 -0
- ksadk/studio/static/assets/index-DXs1Eq4M.css +1 -0
- ksadk/studio/static/assets/index-DeDqwTsD.js +283 -0
- ksadk/studio/static/index.html +2 -2
- ksadk/studio/validator.py +1 -1
- ksadk/studio/workspace.py +1 -0
- ksadk/version.py +1 -1
- ksadk_runtime_common/workspace_files/path_utils.py +41 -0
- ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +0 -1
- ksadk/server/static/assets/channel-4cQHKtx1.js +0 -1
- ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +0 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +0 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +0 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +0 -1
- ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +0 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +0 -1
- ksadk/server/static/assets/index-8ipRcQ-M.js +0 -240
- ksadk/server/static/assets/index-ByFCqjlh.css +0 -2
- ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +0 -1
- ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +0 -1
- ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +0 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +0 -1
- ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +0 -1
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +0 -1
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +0 -1
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +0 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +0 -8
- ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +0 -1
- ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +0 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +0 -1
- ksadk/studio/static/assets/index-BLpcdrgW.css +0 -1
- ksadk/studio/static/assets/index-CYekR2Xv.js +0 -260
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Durable local persistence for Scheduler Lite.
|
|
2
|
+
|
|
3
|
+
This store is deliberately independent from AgentKernel's Inbox tables. It
|
|
4
|
+
owns scheduling decisions, occurrence identity and its *single process*
|
|
5
|
+
lease; AgentControl remains the only execution ingress.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import sqlite3
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from ksadk.scheduler.contracts import (
|
|
18
|
+
ScheduledTask,
|
|
19
|
+
ScheduleOccurrence,
|
|
20
|
+
ScheduleOccurrenceTransition,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_SCHEMA_VERSION = 2
|
|
24
|
+
_ACTIVE_STATES = ("claimed", "accepted", "running")
|
|
25
|
+
_SCHEMA = """
|
|
26
|
+
CREATE TABLE IF NOT EXISTS scheduler_tasks (
|
|
27
|
+
task_id TEXT PRIMARY KEY,
|
|
28
|
+
generation INTEGER NOT NULL,
|
|
29
|
+
body_json TEXT NOT NULL,
|
|
30
|
+
enabled INTEGER NOT NULL,
|
|
31
|
+
next_run_at TEXT,
|
|
32
|
+
created_at TEXT NOT NULL,
|
|
33
|
+
updated_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_scheduler_tasks_due
|
|
36
|
+
ON scheduler_tasks (enabled, next_run_at);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS scheduler_occurrences (
|
|
38
|
+
occurrence_id TEXT PRIMARY KEY,
|
|
39
|
+
task_id TEXT NOT NULL,
|
|
40
|
+
generation INTEGER NOT NULL,
|
|
41
|
+
scheduled_for TEXT NOT NULL,
|
|
42
|
+
trigger_kind TEXT NOT NULL,
|
|
43
|
+
body_json TEXT NOT NULL,
|
|
44
|
+
state TEXT NOT NULL,
|
|
45
|
+
command_id TEXT,
|
|
46
|
+
completed_at TEXT,
|
|
47
|
+
UNIQUE(task_id, generation, scheduled_for, trigger_kind)
|
|
48
|
+
);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_scheduler_occurrences_task_state
|
|
50
|
+
ON scheduler_occurrences (task_id, state, scheduled_for DESC);
|
|
51
|
+
CREATE TABLE IF NOT EXISTS scheduler_leases (
|
|
52
|
+
lease_name TEXT PRIMARY KEY,
|
|
53
|
+
owner_id TEXT NOT NULL,
|
|
54
|
+
expires_at TEXT NOT NULL
|
|
55
|
+
);
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _iso(value: datetime) -> str:
|
|
60
|
+
if value.tzinfo is None:
|
|
61
|
+
raise ValueError("timestamp must include a timezone")
|
|
62
|
+
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _parse(value: str) -> datetime:
|
|
66
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def occurrence_id(
|
|
70
|
+
task_id: str,
|
|
71
|
+
generation: int,
|
|
72
|
+
scheduled_for: datetime,
|
|
73
|
+
trigger: Literal["schedule", "manual"] = "schedule",
|
|
74
|
+
) -> str:
|
|
75
|
+
raw = f"{task_id}\x00{generation}\x00{_iso(scheduled_for)}\x00{trigger}".encode()
|
|
76
|
+
return f"occ_{hashlib.sha256(raw).hexdigest()[:32]}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _session_id(task: ScheduledTask, *, occurrence_id_value: str) -> str:
|
|
80
|
+
if task.continuity == "continue_session":
|
|
81
|
+
assert task.target.session_id is not None
|
|
82
|
+
return task.target.session_id
|
|
83
|
+
# A new scheduled start has an independent durable Session namespace. It
|
|
84
|
+
# is derived from the immutable occurrence id so a retry cannot create a
|
|
85
|
+
# second conversation.
|
|
86
|
+
return f"sched-{occurrence_id_value}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SchedulerSQLiteStore:
|
|
90
|
+
"""Synchronous SQLite store; callers may use it through ``to_thread``.
|
|
91
|
+
|
|
92
|
+
Every mutation uses its own connection and ``BEGIN IMMEDIATE``. That is
|
|
93
|
+
intentional: this component has no undeclared aiosqlite dependency and
|
|
94
|
+
remains correct when multiple local Studio processes contend for its DB.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, path: str | Path) -> None:
|
|
98
|
+
self.path = Path(path).expanduser().resolve()
|
|
99
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
self.ensure_schema()
|
|
101
|
+
|
|
102
|
+
def _connect(self) -> sqlite3.Connection:
|
|
103
|
+
connection = sqlite3.connect(self.path, timeout=5, isolation_level=None)
|
|
104
|
+
connection.row_factory = sqlite3.Row
|
|
105
|
+
connection.execute("PRAGMA journal_mode=WAL")
|
|
106
|
+
connection.execute("PRAGMA synchronous=FULL")
|
|
107
|
+
return connection
|
|
108
|
+
|
|
109
|
+
def ensure_schema(self) -> None:
|
|
110
|
+
with self._connect() as connection:
|
|
111
|
+
connection.executescript(_SCHEMA)
|
|
112
|
+
connection.execute(f"PRAGMA user_version={_SCHEMA_VERSION}")
|
|
113
|
+
|
|
114
|
+
def put_task(self, task: ScheduledTask, *, generation: int | None = None) -> int:
|
|
115
|
+
now = _iso(datetime.now(timezone.utc))
|
|
116
|
+
encoded = task.model_dump_json(by_alias=True, exclude_none=True)
|
|
117
|
+
with self._connect() as connection:
|
|
118
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
119
|
+
existing = connection.execute(
|
|
120
|
+
"SELECT generation, created_at FROM scheduler_tasks WHERE task_id=?",
|
|
121
|
+
(task.task_id,),
|
|
122
|
+
).fetchone()
|
|
123
|
+
actual_generation = generation
|
|
124
|
+
if actual_generation is None:
|
|
125
|
+
actual_generation = int(existing["generation"]) + 1 if existing else 1
|
|
126
|
+
created_at = existing["created_at"] if existing else _iso(task.created_at)
|
|
127
|
+
connection.execute(
|
|
128
|
+
"""INSERT INTO scheduler_tasks(
|
|
129
|
+
task_id,generation,body_json,enabled,next_run_at,created_at,updated_at
|
|
130
|
+
)
|
|
131
|
+
VALUES(?,?,?,?,?,?,?)
|
|
132
|
+
ON CONFLICT(task_id) DO UPDATE SET generation=excluded.generation,
|
|
133
|
+
body_json=excluded.body_json,enabled=excluded.enabled,next_run_at=excluded.next_run_at,
|
|
134
|
+
updated_at=excluded.updated_at""",
|
|
135
|
+
(
|
|
136
|
+
task.task_id,
|
|
137
|
+
actual_generation,
|
|
138
|
+
encoded,
|
|
139
|
+
int(task.enabled),
|
|
140
|
+
_iso(task.next_run_at) if task.next_run_at else None,
|
|
141
|
+
created_at,
|
|
142
|
+
now,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
connection.commit()
|
|
146
|
+
return actual_generation
|
|
147
|
+
|
|
148
|
+
def get_task(self, task_id: str) -> tuple[ScheduledTask, int] | None:
|
|
149
|
+
with self._connect() as connection:
|
|
150
|
+
row = connection.execute(
|
|
151
|
+
"SELECT body_json,generation,next_run_at,enabled "
|
|
152
|
+
"FROM scheduler_tasks WHERE task_id=?",
|
|
153
|
+
(task_id,),
|
|
154
|
+
).fetchone()
|
|
155
|
+
if row is None:
|
|
156
|
+
return None
|
|
157
|
+
body = json.loads(row["body_json"])
|
|
158
|
+
body["nextRunAt"] = row["next_run_at"]
|
|
159
|
+
body["enabled"] = bool(row["enabled"])
|
|
160
|
+
return ScheduledTask.model_validate(body), int(row["generation"])
|
|
161
|
+
|
|
162
|
+
def list_due(self, now: datetime) -> list[tuple[ScheduledTask, int]]:
|
|
163
|
+
with self._connect() as connection:
|
|
164
|
+
rows = connection.execute(
|
|
165
|
+
"""SELECT body_json,generation,next_run_at FROM scheduler_tasks
|
|
166
|
+
WHERE enabled=1 AND next_run_at IS NOT NULL AND next_run_at<=?
|
|
167
|
+
ORDER BY next_run_at,task_id""",
|
|
168
|
+
(_iso(now),),
|
|
169
|
+
).fetchall()
|
|
170
|
+
result: list[tuple[ScheduledTask, int]] = []
|
|
171
|
+
for row in rows:
|
|
172
|
+
body = json.loads(row["body_json"])
|
|
173
|
+
body["nextRunAt"] = row["next_run_at"]
|
|
174
|
+
result.append((ScheduledTask.model_validate(body), int(row["generation"])))
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
def list_tasks(self) -> list[tuple[ScheduledTask, int]]:
|
|
178
|
+
with self._connect() as connection:
|
|
179
|
+
rows = connection.execute(
|
|
180
|
+
"SELECT body_json,generation,next_run_at,enabled FROM scheduler_tasks "
|
|
181
|
+
"ORDER BY updated_at DESC,task_id"
|
|
182
|
+
).fetchall()
|
|
183
|
+
result: list[tuple[ScheduledTask, int]] = []
|
|
184
|
+
for row in rows:
|
|
185
|
+
body = json.loads(row["body_json"])
|
|
186
|
+
body["nextRunAt"] = row["next_run_at"]
|
|
187
|
+
body["enabled"] = bool(row["enabled"])
|
|
188
|
+
result.append((ScheduledTask.model_validate(body), int(row["generation"])))
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
def delete_task(self, task_id: str) -> bool:
|
|
192
|
+
"""Remove the scheduling intent but retain immutable occurrence history."""
|
|
193
|
+
|
|
194
|
+
with self._connect() as connection:
|
|
195
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
196
|
+
removed = connection.execute(
|
|
197
|
+
"DELETE FROM scheduler_tasks WHERE task_id=?", (task_id,)
|
|
198
|
+
).rowcount
|
|
199
|
+
connection.commit()
|
|
200
|
+
return removed == 1
|
|
201
|
+
|
|
202
|
+
def list_occurrences(self, task_id: str, *, limit: int = 50) -> list[ScheduleOccurrence]:
|
|
203
|
+
if limit < 1 or limit > 200:
|
|
204
|
+
raise ValueError("occurrence limit must be between 1 and 200")
|
|
205
|
+
with self._connect() as connection:
|
|
206
|
+
rows = connection.execute(
|
|
207
|
+
"""SELECT body_json FROM scheduler_occurrences WHERE task_id=?
|
|
208
|
+
ORDER BY scheduled_for DESC,occurrence_id DESC LIMIT ?""",
|
|
209
|
+
(task_id, limit),
|
|
210
|
+
).fetchall()
|
|
211
|
+
return [ScheduleOccurrence.model_validate_json(row["body_json"]) for row in rows]
|
|
212
|
+
|
|
213
|
+
def list_all_occurrences(self, *, limit: int = 200) -> list[ScheduleOccurrence]:
|
|
214
|
+
"""Return cross-task history, including records whose task was deleted."""
|
|
215
|
+
|
|
216
|
+
if limit < 1 or limit > 500:
|
|
217
|
+
raise ValueError("occurrence limit must be between 1 and 500")
|
|
218
|
+
with self._connect() as connection:
|
|
219
|
+
rows = connection.execute(
|
|
220
|
+
"SELECT body_json FROM scheduler_occurrences "
|
|
221
|
+
"ORDER BY scheduled_for DESC,occurrence_id DESC LIMIT ?",
|
|
222
|
+
(limit,),
|
|
223
|
+
).fetchall()
|
|
224
|
+
return [ScheduleOccurrence.model_validate_json(row["body_json"]) for row in rows]
|
|
225
|
+
|
|
226
|
+
def list_active_occurrences(self, *, limit: int = 200) -> list[ScheduleOccurrence]:
|
|
227
|
+
"""Return only non-terminal occurrences for event reconciliation.
|
|
228
|
+
|
|
229
|
+
The target snapshot is stored on the occurrence itself, so deleting a
|
|
230
|
+
task deliberately does not orphan a previously accepted execution.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
if limit < 1 or limit > 200:
|
|
234
|
+
raise ValueError("active occurrence limit must be between 1 and 200")
|
|
235
|
+
placeholders = ",".join("?" for _ in _ACTIVE_STATES)
|
|
236
|
+
with self._connect() as connection:
|
|
237
|
+
rows = connection.execute(
|
|
238
|
+
"SELECT body_json FROM scheduler_occurrences "
|
|
239
|
+
f"WHERE state IN ({placeholders}) "
|
|
240
|
+
"ORDER BY scheduled_for,occurrence_id LIMIT ?",
|
|
241
|
+
(*_ACTIVE_STATES, limit),
|
|
242
|
+
).fetchall()
|
|
243
|
+
return [ScheduleOccurrence.model_validate_json(row["body_json"]) for row in rows]
|
|
244
|
+
|
|
245
|
+
def has_active_occurrence(self, task_id: str) -> bool:
|
|
246
|
+
placeholders = ",".join("?" for _ in _ACTIVE_STATES)
|
|
247
|
+
with self._connect() as connection:
|
|
248
|
+
row = connection.execute(
|
|
249
|
+
"SELECT 1 FROM scheduler_occurrences "
|
|
250
|
+
f"WHERE task_id=? AND state IN ({placeholders}) LIMIT 1",
|
|
251
|
+
(task_id, *_ACTIVE_STATES),
|
|
252
|
+
).fetchone()
|
|
253
|
+
return row is not None
|
|
254
|
+
|
|
255
|
+
def claim_and_advance(
|
|
256
|
+
self,
|
|
257
|
+
task: ScheduledTask,
|
|
258
|
+
*,
|
|
259
|
+
generation: int,
|
|
260
|
+
next_run_at: datetime | None,
|
|
261
|
+
state: Literal["claimed", "skipped"] = "claimed",
|
|
262
|
+
detail: str | None = None,
|
|
263
|
+
trigger: Literal["schedule", "manual"] = "schedule",
|
|
264
|
+
claimed_at: datetime | None = None,
|
|
265
|
+
) -> ScheduleOccurrence | None:
|
|
266
|
+
if task.next_run_at is None:
|
|
267
|
+
raise ValueError("task must have next_run_at before it can be claimed")
|
|
268
|
+
scheduled_for = task.next_run_at
|
|
269
|
+
now = claimed_at or datetime.now(timezone.utc)
|
|
270
|
+
completed_at = now if state == "skipped" else None
|
|
271
|
+
transitions = [ScheduleOccurrenceTransition(state="claimed", at=now)]
|
|
272
|
+
if state == "skipped":
|
|
273
|
+
transitions.append(ScheduleOccurrenceTransition(state="skipped", at=now, detail=detail))
|
|
274
|
+
occurrence = ScheduleOccurrence(
|
|
275
|
+
occurrence_id=occurrence_id(task.task_id, generation, scheduled_for, trigger),
|
|
276
|
+
task_id=task.task_id,
|
|
277
|
+
target=task.target,
|
|
278
|
+
scheduled_for=scheduled_for,
|
|
279
|
+
session_id=_session_id(
|
|
280
|
+
task,
|
|
281
|
+
occurrence_id_value=occurrence_id(task.task_id, generation, scheduled_for, trigger),
|
|
282
|
+
),
|
|
283
|
+
trigger=trigger,
|
|
284
|
+
state=state,
|
|
285
|
+
claimed_at=now,
|
|
286
|
+
detail=detail,
|
|
287
|
+
completed_at=completed_at,
|
|
288
|
+
transitions=tuple(transitions),
|
|
289
|
+
)
|
|
290
|
+
with self._connect() as connection:
|
|
291
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
292
|
+
updated = connection.execute(
|
|
293
|
+
"""UPDATE scheduler_tasks SET next_run_at=?,updated_at=?
|
|
294
|
+
WHERE task_id=? AND generation=? AND next_run_at=? AND enabled=1""",
|
|
295
|
+
(
|
|
296
|
+
_iso(next_run_at) if next_run_at else None,
|
|
297
|
+
_iso(now),
|
|
298
|
+
task.task_id,
|
|
299
|
+
generation,
|
|
300
|
+
_iso(scheduled_for),
|
|
301
|
+
),
|
|
302
|
+
).rowcount
|
|
303
|
+
if updated != 1:
|
|
304
|
+
connection.rollback()
|
|
305
|
+
return None
|
|
306
|
+
connection.execute(
|
|
307
|
+
"""INSERT OR IGNORE INTO scheduler_occurrences(
|
|
308
|
+
occurrence_id,task_id,generation,scheduled_for,trigger_kind,body_json,state,command_id,completed_at)
|
|
309
|
+
VALUES(?,?,?,?,?,?,?,?,?)""",
|
|
310
|
+
(
|
|
311
|
+
occurrence.occurrence_id,
|
|
312
|
+
task.task_id,
|
|
313
|
+
generation,
|
|
314
|
+
_iso(scheduled_for),
|
|
315
|
+
trigger,
|
|
316
|
+
occurrence.model_dump_json(by_alias=True, exclude_none=True),
|
|
317
|
+
state,
|
|
318
|
+
None,
|
|
319
|
+
_iso(completed_at) if completed_at else None,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
connection.commit()
|
|
323
|
+
return occurrence
|
|
324
|
+
|
|
325
|
+
def mark_accepted(
|
|
326
|
+
self,
|
|
327
|
+
occurrence_id_value: str,
|
|
328
|
+
command_id: str,
|
|
329
|
+
*,
|
|
330
|
+
accepted_seq: int | None = None,
|
|
331
|
+
) -> ScheduleOccurrence:
|
|
332
|
+
return self._transition(
|
|
333
|
+
occurrence_id_value,
|
|
334
|
+
state="accepted",
|
|
335
|
+
command_id=command_id,
|
|
336
|
+
accepted_seq=accepted_seq,
|
|
337
|
+
last_event_seq=accepted_seq,
|
|
338
|
+
accepted_at=datetime.now(timezone.utc),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def claim_manual(
|
|
342
|
+
self,
|
|
343
|
+
task: ScheduledTask,
|
|
344
|
+
*,
|
|
345
|
+
generation: int,
|
|
346
|
+
now: datetime,
|
|
347
|
+
) -> ScheduleOccurrence | None:
|
|
348
|
+
"""Create a durable manual occurrence without changing the timetable."""
|
|
349
|
+
|
|
350
|
+
occurrence = ScheduleOccurrence(
|
|
351
|
+
occurrence_id=occurrence_id(task.task_id, generation, now, "manual"),
|
|
352
|
+
task_id=task.task_id,
|
|
353
|
+
target=task.target,
|
|
354
|
+
scheduled_for=now,
|
|
355
|
+
session_id=_session_id(
|
|
356
|
+
task,
|
|
357
|
+
occurrence_id_value=occurrence_id(task.task_id, generation, now, "manual"),
|
|
358
|
+
),
|
|
359
|
+
trigger="manual",
|
|
360
|
+
state="claimed",
|
|
361
|
+
claimed_at=now,
|
|
362
|
+
transitions=(ScheduleOccurrenceTransition(state="claimed", at=now),),
|
|
363
|
+
)
|
|
364
|
+
with self._connect() as connection:
|
|
365
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
366
|
+
current = connection.execute(
|
|
367
|
+
"SELECT generation,enabled FROM scheduler_tasks WHERE task_id=?",
|
|
368
|
+
(task.task_id,),
|
|
369
|
+
).fetchone()
|
|
370
|
+
if (
|
|
371
|
+
current is None
|
|
372
|
+
or int(current["generation"]) != generation
|
|
373
|
+
or not current["enabled"]
|
|
374
|
+
):
|
|
375
|
+
connection.rollback()
|
|
376
|
+
return None
|
|
377
|
+
inserted = connection.execute(
|
|
378
|
+
"""INSERT OR IGNORE INTO scheduler_occurrences(
|
|
379
|
+
occurrence_id,task_id,generation,scheduled_for,trigger_kind,
|
|
380
|
+
body_json,state,command_id,completed_at
|
|
381
|
+
) VALUES(?,?,?,?,?,?,?,?,?)""",
|
|
382
|
+
(
|
|
383
|
+
occurrence.occurrence_id,
|
|
384
|
+
task.task_id,
|
|
385
|
+
generation,
|
|
386
|
+
_iso(now),
|
|
387
|
+
"manual",
|
|
388
|
+
occurrence.model_dump_json(by_alias=True, exclude_none=True),
|
|
389
|
+
occurrence.state,
|
|
390
|
+
None,
|
|
391
|
+
None,
|
|
392
|
+
),
|
|
393
|
+
).rowcount
|
|
394
|
+
if inserted != 1:
|
|
395
|
+
connection.rollback()
|
|
396
|
+
return None
|
|
397
|
+
connection.commit()
|
|
398
|
+
return occurrence
|
|
399
|
+
|
|
400
|
+
def finish(
|
|
401
|
+
self,
|
|
402
|
+
occurrence_id_value: str,
|
|
403
|
+
*,
|
|
404
|
+
succeeded: bool,
|
|
405
|
+
error_code: str | None = None,
|
|
406
|
+
detail: str | None = None,
|
|
407
|
+
) -> ScheduleOccurrence:
|
|
408
|
+
if not succeeded and not error_code:
|
|
409
|
+
raise ValueError("failed scheduler occurrence requires error_code")
|
|
410
|
+
return self._transition(
|
|
411
|
+
occurrence_id_value,
|
|
412
|
+
state="succeeded" if succeeded else "failed",
|
|
413
|
+
error_code=error_code,
|
|
414
|
+
detail=detail,
|
|
415
|
+
completed_at=datetime.now(timezone.utc),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def mark_running(
|
|
419
|
+
self,
|
|
420
|
+
occurrence_id_value: str,
|
|
421
|
+
*,
|
|
422
|
+
run_id: str,
|
|
423
|
+
last_event_seq: int,
|
|
424
|
+
) -> ScheduleOccurrence:
|
|
425
|
+
return self._transition(
|
|
426
|
+
occurrence_id_value,
|
|
427
|
+
state="running",
|
|
428
|
+
run_id=run_id,
|
|
429
|
+
started_at=datetime.now(timezone.utc),
|
|
430
|
+
last_event_seq=last_event_seq,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def bind_run(
|
|
434
|
+
self,
|
|
435
|
+
occurrence_id_value: str,
|
|
436
|
+
*,
|
|
437
|
+
run_id: str,
|
|
438
|
+
last_event_seq: int,
|
|
439
|
+
) -> ScheduleOccurrence:
|
|
440
|
+
"""Record a durable run identity before that run becomes runnable."""
|
|
441
|
+
|
|
442
|
+
return self._transition(
|
|
443
|
+
occurrence_id_value,
|
|
444
|
+
run_id=run_id,
|
|
445
|
+
last_event_seq=last_event_seq,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def advance_reconciliation_cursor(
|
|
449
|
+
self, occurrence_id_value: str, *, last_event_seq: int
|
|
450
|
+
) -> ScheduleOccurrence:
|
|
451
|
+
return self._transition(
|
|
452
|
+
occurrence_id_value,
|
|
453
|
+
last_event_seq=last_event_seq,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def _transition(self, occurrence_id_value: str, **updates: object) -> ScheduleOccurrence:
|
|
457
|
+
with self._connect() as connection:
|
|
458
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
459
|
+
row = connection.execute(
|
|
460
|
+
"SELECT body_json,state FROM scheduler_occurrences WHERE occurrence_id=?",
|
|
461
|
+
(occurrence_id_value,),
|
|
462
|
+
).fetchone()
|
|
463
|
+
if row is None:
|
|
464
|
+
connection.rollback()
|
|
465
|
+
raise KeyError(occurrence_id_value)
|
|
466
|
+
body = json.loads(row["body_json"])
|
|
467
|
+
current = ScheduleOccurrence.model_validate(body)
|
|
468
|
+
terminal = {"succeeded", "failed", "skipped", "cancelled"}
|
|
469
|
+
# Terminal facts are first-wins. A replayed SSE frame or a later
|
|
470
|
+
# contradictory event must never rewrite auditable scheduler
|
|
471
|
+
# history after it has reached a conclusion.
|
|
472
|
+
if current.state in terminal:
|
|
473
|
+
connection.rollback()
|
|
474
|
+
return current
|
|
475
|
+
current_seq = current.last_event_seq
|
|
476
|
+
requested_seq = updates.get("last_event_seq")
|
|
477
|
+
if (
|
|
478
|
+
isinstance(requested_seq, int)
|
|
479
|
+
and current_seq is not None
|
|
480
|
+
and requested_seq < current_seq
|
|
481
|
+
):
|
|
482
|
+
connection.rollback()
|
|
483
|
+
return current
|
|
484
|
+
# Stored JSON uses the public camelCase aliases. Updating with a
|
|
485
|
+
# Python field name while an older alias is present would create
|
|
486
|
+
# two values for one field and Pydantic correctly rejects it as an
|
|
487
|
+
# ambiguous/extra input. Normalize every mutation at this one
|
|
488
|
+
# persistence boundary instead.
|
|
489
|
+
for key, value in updates.items():
|
|
490
|
+
if value is None:
|
|
491
|
+
continue
|
|
492
|
+
field = ScheduleOccurrence.model_fields.get(key)
|
|
493
|
+
encoded_key = field.alias if field and field.alias else key
|
|
494
|
+
body[encoded_key] = _iso(value) if isinstance(value, datetime) else value
|
|
495
|
+
requested_state = updates.get("state")
|
|
496
|
+
if isinstance(requested_state, str) and requested_state != current.state:
|
|
497
|
+
transition_at = next(
|
|
498
|
+
(
|
|
499
|
+
value
|
|
500
|
+
for name in ("completed_at", "started_at", "accepted_at")
|
|
501
|
+
if isinstance((value := updates.get(name)), datetime)
|
|
502
|
+
),
|
|
503
|
+
datetime.now(timezone.utc),
|
|
504
|
+
)
|
|
505
|
+
transitions = list(current.transitions)
|
|
506
|
+
if transitions and transition_at < transitions[-1].at:
|
|
507
|
+
# Custom/test clocks and small host clock corrections must
|
|
508
|
+
# not make a durable audit timeline go backwards.
|
|
509
|
+
transition_at = transitions[-1].at
|
|
510
|
+
transitions.append(
|
|
511
|
+
ScheduleOccurrenceTransition(
|
|
512
|
+
state=requested_state, # type: ignore[arg-type]
|
|
513
|
+
at=transition_at,
|
|
514
|
+
detail=(str(updates["detail"]) if updates.get("detail") else None),
|
|
515
|
+
error_code=(
|
|
516
|
+
str(updates["error_code"]) if updates.get("error_code") else None
|
|
517
|
+
),
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
body["transitions"] = [
|
|
521
|
+
item.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
522
|
+
for item in transitions
|
|
523
|
+
]
|
|
524
|
+
occurrence = ScheduleOccurrence.model_validate(body)
|
|
525
|
+
connection.execute(
|
|
526
|
+
"""UPDATE scheduler_occurrences SET body_json=?,state=?,command_id=?,completed_at=?
|
|
527
|
+
WHERE occurrence_id=?""",
|
|
528
|
+
(
|
|
529
|
+
occurrence.model_dump_json(by_alias=True, exclude_none=True),
|
|
530
|
+
occurrence.state,
|
|
531
|
+
occurrence.command_id,
|
|
532
|
+
_iso(occurrence.completed_at) if occurrence.completed_at else None,
|
|
533
|
+
occurrence_id_value,
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
connection.commit()
|
|
537
|
+
return occurrence
|
|
538
|
+
|
|
539
|
+
def acquire_lease(self, *, owner_id: str, now: datetime, ttl_seconds: int = 30) -> bool:
|
|
540
|
+
expiry = now.astimezone(timezone.utc).timestamp() + ttl_seconds
|
|
541
|
+
expires_at = datetime.fromtimestamp(expiry, tz=timezone.utc)
|
|
542
|
+
with self._connect() as connection:
|
|
543
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
544
|
+
row = connection.execute(
|
|
545
|
+
"SELECT owner_id,expires_at FROM scheduler_leases "
|
|
546
|
+
"WHERE lease_name='local-scheduler'"
|
|
547
|
+
).fetchone()
|
|
548
|
+
allowed = row is None or _parse(row["expires_at"]) <= now or row["owner_id"] == owner_id
|
|
549
|
+
if allowed:
|
|
550
|
+
connection.execute(
|
|
551
|
+
"""INSERT INTO scheduler_leases(lease_name,owner_id,expires_at)
|
|
552
|
+
VALUES('local-scheduler',?,?)
|
|
553
|
+
ON CONFLICT(lease_name) DO UPDATE SET
|
|
554
|
+
owner_id=excluded.owner_id,expires_at=excluded.expires_at""",
|
|
555
|
+
(owner_id, _iso(expires_at)),
|
|
556
|
+
)
|
|
557
|
+
connection.commit()
|
|
558
|
+
return True
|
|
559
|
+
connection.rollback()
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
__all__ = ["SchedulerSQLiteStore", "occurrence_id"]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{St as e,Tt as t,bt as n,gt as r,lt as i,n as a,t as o,ut as s,xt as c}from"./index-
|
|
1
|
+
import{St as e,Tt as t,bt as n,gt as r,lt as i,n as a,t as o,ut as s,xt as c}from"./index-B2k_urY8.js";var l=t(e(),1),u=n();function d(){let e=c(e=>e.visible),t=c(e=>e.content),n=c(e=>e.type),d=c(e=>e.hide),f=(0,l.useRef)(null);if(a(f),!e||!t)return null;let p=o(t);return(0,u.jsxs)(`aside`,{className:i(`flex h-full flex-shrink-0 flex-col border-l border-slate-200/30 bg-white dark:border-slate-800/40 dark:bg-slate-950`,`w-[min(42rem,50vw)]`),children:[(0,u.jsxs)(`div`,{className:`flex h-14 flex-shrink-0 items-center justify-between border-b border-slate-200/30 px-4 dark:border-slate-800/40`,children:[(0,u.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2`,children:[(0,u.jsx)(`span`,{className:`text-sm font-semibold text-slate-900 dark:text-slate-100`,children:`Artifact`}),(0,u.jsx)(`span`,{className:`rounded-md bg-slate-100 px-1.5 py-0.5 font-mono text-[11px] text-slate-500 dark:bg-slate-900 dark:text-slate-400`,children:n})]}),(0,u.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,u.jsxs)(`button`,{type:`button`,onClick:()=>{let e=new Blob([t],{type:`text/html;charset=utf-8`}),r=URL.createObjectURL(e),i=document.createElement(`a`);i.href=r,i.download=`artifact.${n===`svg`?`svg`:`html`}`,document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(r)},className:`inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-600 transition hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-900`,title:`Download`,children:[(0,u.jsx)(r,{className:`h-3.5 w-3.5`}),`Download`]}),(0,u.jsx)(`button`,{type:`button`,onClick:d,className:`rounded-lg p-1.5 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-900 dark:hover:text-slate-100`,"aria-label":`Close Artifact`,title:`Close`,children:(0,u.jsx)(s,{className:`h-4 w-4`})})]})]}),(0,u.jsx)(`div`,{className:`min-h-0 flex-1`,children:(0,u.jsx)(`iframe`,{ref:f,srcDoc:p,sandbox:`allow-scripts allow-downloads`,title:`Artifact Preview`,className:`h-full w-full border-0 bg-white`})})]})}export{d as ArtifactsPanel};
|