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
ksadk/runtime/runner_adapter.py
CHANGED
|
@@ -185,6 +185,11 @@ class RunnerRuntimeAdapter(_RunnerStreamMappingMixin, RuntimeAdapter):
|
|
|
185
185
|
if durable_supported
|
|
186
186
|
else _unavailable("durable_restore_requires_cross_process_checkpoint")
|
|
187
187
|
),
|
|
188
|
+
# A generic Runner cannot claim interaction delivery merely from a
|
|
189
|
+
# checkpoint capability. ADK is forward-only and only the
|
|
190
|
+
# LangGraph specialization below binds a checkpoint to the
|
|
191
|
+
# original interrupt identity.
|
|
192
|
+
interaction_mode="unavailable",
|
|
188
193
|
)
|
|
189
194
|
|
|
190
195
|
async def durable_restore(self, handle: RunHandle) -> RunHandle:
|
ksadk/sandbox/__init__.py
CHANGED
|
@@ -12,7 +12,7 @@ from ksadk.sandbox.base import (
|
|
|
12
12
|
SandboxSpec,
|
|
13
13
|
SandboxType,
|
|
14
14
|
)
|
|
15
|
-
from ksadk.sandbox.factory import create_sandbox_backend, sandbox_spec_from_env
|
|
15
|
+
from ksadk.sandbox.factory import create_sandbox_backend, sandbox_spec_from_env, setup_sandbox_api_url_if_needed
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"E2BSandboxBackend",
|
|
@@ -28,4 +28,5 @@ __all__ = [
|
|
|
28
28
|
"SandboxType",
|
|
29
29
|
"create_sandbox_backend",
|
|
30
30
|
"sandbox_spec_from_env",
|
|
31
|
+
"setup_sandbox_api_url_if_needed",
|
|
31
32
|
]
|
ksadk/sandbox/factory.py
CHANGED
|
@@ -16,11 +16,83 @@ def bool_env(name: str, default: bool = True) -> bool:
|
|
|
16
16
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def _resolve_sandbox_api_url() -> str:
|
|
20
|
+
"""Probe sandbox control-plane URL at runtime (in-Pod).
|
|
21
|
+
|
|
22
|
+
agentengine-server injects public E2B_API_URL on the management cluster.
|
|
23
|
+
public_only compute pods can reach the public address directly; private_only
|
|
24
|
+
pods have no public egress. Strategy: try the original (public) URL first;
|
|
25
|
+
if reachable keep it, otherwise fall back to the 198 public-service-net
|
|
26
|
+
internal address. Mirrors KSPMAS get_kspmas_api_base(): probe runs in-Pod.
|
|
27
|
+
"""
|
|
28
|
+
raw = (os.environ.get("E2B_API_URL") or "").strip()
|
|
29
|
+
if not raw:
|
|
30
|
+
return raw
|
|
31
|
+
|
|
32
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
33
|
+
|
|
34
|
+
parsed = urlsplit(raw)
|
|
35
|
+
host = (parsed.hostname or "").lower()
|
|
36
|
+
if host.endswith(".sdns.ksyun.com"):
|
|
37
|
+
return raw
|
|
38
|
+
if not host.endswith(".sandbox.ksyun.com"):
|
|
39
|
+
return raw
|
|
40
|
+
|
|
41
|
+
parts = host.split(".")
|
|
42
|
+
region = ""
|
|
43
|
+
if len(parts) >= 4 and parts[0] == "mgr":
|
|
44
|
+
region = parts[1]
|
|
45
|
+
elif len(parts) >= 4:
|
|
46
|
+
region = parts[1] if parts[1] != "sandbox" else ""
|
|
47
|
+
if not region:
|
|
48
|
+
return raw
|
|
49
|
+
|
|
50
|
+
import socket
|
|
51
|
+
|
|
52
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
53
|
+
|
|
54
|
+
# 1) Try the original (public) URL first; reachable => keep it.
|
|
55
|
+
try:
|
|
56
|
+
s = socket.socket()
|
|
57
|
+
s.settimeout(1.5)
|
|
58
|
+
s.connect((host, port))
|
|
59
|
+
s.close()
|
|
60
|
+
return raw
|
|
61
|
+
except OSError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# 2) Public unreachable (e.g. private_only node) => fall back to internal.
|
|
65
|
+
internal_host = f"sandbox.{region}.sandbox.sdns.ksyun.com"
|
|
66
|
+
try:
|
|
67
|
+
s = socket.socket()
|
|
68
|
+
s.settimeout(1.5)
|
|
69
|
+
s.connect((internal_host, port))
|
|
70
|
+
s.close()
|
|
71
|
+
except OSError:
|
|
72
|
+
return raw
|
|
73
|
+
|
|
74
|
+
internal_url = urlunsplit((parsed.scheme or "https", internal_host, parsed.path, parsed.query, ""))
|
|
75
|
+
return internal_url
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def setup_sandbox_api_url_if_needed() -> None:
|
|
79
|
+
"""Override E2B_API_URL to internal address before creating E2B backend.
|
|
80
|
+
|
|
81
|
+
Only runs in managed runtime (AGENT_RUNTIME_ID / K8S); local dev unaffected.
|
|
82
|
+
"""
|
|
83
|
+
if not os.environ.get("AGENT_RUNTIME_ID") and not os.environ.get("KUBERNETES_SERVICE_HOST"):
|
|
84
|
+
return
|
|
85
|
+
resolved = _resolve_sandbox_api_url()
|
|
86
|
+
if resolved != (os.environ.get("E2B_API_URL") or ""):
|
|
87
|
+
os.environ["E2B_API_URL"] = resolved
|
|
88
|
+
|
|
89
|
+
|
|
19
90
|
def create_sandbox_backend(
|
|
20
91
|
backend: str | None = None,
|
|
21
92
|
*,
|
|
22
93
|
sandbox_cls: Any | None = None,
|
|
23
94
|
) -> SandboxBackend:
|
|
95
|
+
setup_sandbox_api_url_if_needed()
|
|
24
96
|
resolved = (backend or os.environ.get("KSADK_SANDBOX_BACKEND") or "e2b").strip().lower()
|
|
25
97
|
if resolved in {"local", "local_process"}:
|
|
26
98
|
return LocalProcessSandboxBackend(
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Local Scheduler Lite contracts and deterministic calendar calculations."""
|
|
2
|
+
|
|
3
|
+
from ksadk.scheduler.calendar import next_schedule_time
|
|
4
|
+
from ksadk.scheduler.contracts import ScheduledTask, ScheduleOccurrence, ScheduleSpec
|
|
5
|
+
from ksadk.scheduler.dispatcher import AgentControlSchedulerDispatcher
|
|
6
|
+
from ksadk.scheduler.engine import SchedulerDispatchError, SchedulerEngine
|
|
7
|
+
from ksadk.scheduler.sqlite_store import SchedulerSQLiteStore
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ScheduleOccurrence",
|
|
11
|
+
"ScheduledTask",
|
|
12
|
+
"ScheduleSpec",
|
|
13
|
+
"AgentControlSchedulerDispatcher",
|
|
14
|
+
"SchedulerDispatchError",
|
|
15
|
+
"SchedulerEngine",
|
|
16
|
+
"SchedulerSQLiteStore",
|
|
17
|
+
"next_schedule_time",
|
|
18
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Deterministic, dependency-free calendar calculations for Scheduler Lite.
|
|
2
|
+
|
|
3
|
+
The supported cron dialect intentionally stays small and explicit: five fields,
|
|
4
|
+
numeric values, comma lists, ranges and ``*/step``. It is sufficient for the
|
|
5
|
+
Studio scheduler and avoids inheriting an accidental transitive dependency.
|
|
6
|
+
All matching happens after UTC -> IANA timezone conversion. That means a
|
|
7
|
+
nonexistent DST local minute is skipped and the repeated fall-back minute runs
|
|
8
|
+
once (the first ``fold`` only).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from datetime import datetime, timedelta, timezone
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
15
|
+
|
|
16
|
+
from ksadk.scheduler.contracts import ScheduleSpec, _parse_cron
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def next_schedule_time(
|
|
20
|
+
schedule: ScheduleSpec,
|
|
21
|
+
*,
|
|
22
|
+
after: datetime,
|
|
23
|
+
anchor_at: datetime | None = None,
|
|
24
|
+
) -> datetime | None:
|
|
25
|
+
"""Return the first scheduled UTC timestamp strictly after ``after``."""
|
|
26
|
+
|
|
27
|
+
if after.tzinfo is None:
|
|
28
|
+
raise ValueError("after must include a timezone")
|
|
29
|
+
after = after.astimezone(timezone.utc)
|
|
30
|
+
if schedule.kind == "once":
|
|
31
|
+
assert schedule.at is not None
|
|
32
|
+
return schedule.at if schedule.at > after else None
|
|
33
|
+
if schedule.kind == "interval":
|
|
34
|
+
assert schedule.every_seconds is not None
|
|
35
|
+
anchor = (schedule.anchor_at or anchor_at or after).astimezone(timezone.utc)
|
|
36
|
+
if anchor > after:
|
|
37
|
+
return anchor
|
|
38
|
+
elapsed = (after - anchor).total_seconds()
|
|
39
|
+
step = schedule.every_seconds
|
|
40
|
+
return anchor + timedelta(seconds=(int(elapsed // step) + 1) * step)
|
|
41
|
+
assert schedule.expression is not None
|
|
42
|
+
return _next_cron(schedule.expression, schedule.timezone, after)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _next_cron(expression: str, timezone_name: str, after: datetime) -> datetime:
|
|
46
|
+
minute, hour, day, month, weekday = _parse_cron(expression)
|
|
47
|
+
fields = (
|
|
48
|
+
_expand_field(minute, 0, 59),
|
|
49
|
+
_expand_field(hour, 0, 23),
|
|
50
|
+
_expand_field(day, 1, 31),
|
|
51
|
+
_expand_field(month, 1, 12),
|
|
52
|
+
_expand_field(weekday, 0, 6),
|
|
53
|
+
)
|
|
54
|
+
zone = ZoneInfo(timezone_name)
|
|
55
|
+
candidate = after.replace(second=0, microsecond=0) + timedelta(minutes=1)
|
|
56
|
+
# Five years is a deliberate guard against impossible expressions such as
|
|
57
|
+
# 31 February. The caller receives a typed validation error rather than a
|
|
58
|
+
# scheduler loop that runs forever.
|
|
59
|
+
deadline = candidate + timedelta(days=366 * 5)
|
|
60
|
+
while candidate <= deadline:
|
|
61
|
+
local = candidate.astimezone(zone)
|
|
62
|
+
# datetime.weekday uses Monday=0. Cron uses Sunday=0.
|
|
63
|
+
cron_weekday = (local.weekday() + 1) % 7
|
|
64
|
+
if (
|
|
65
|
+
local.fold == 0
|
|
66
|
+
and local.minute in fields[0]
|
|
67
|
+
and local.hour in fields[1]
|
|
68
|
+
and local.day in fields[2]
|
|
69
|
+
and local.month in fields[3]
|
|
70
|
+
and cron_weekday in fields[4]
|
|
71
|
+
):
|
|
72
|
+
return candidate
|
|
73
|
+
candidate += timedelta(minutes=1)
|
|
74
|
+
raise ValueError("cron expression has no matching time within five years")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _expand_field(value: str, minimum: int, maximum: int) -> set[int]:
|
|
78
|
+
result: set[int] = set()
|
|
79
|
+
for item in value.split(","):
|
|
80
|
+
if "/" in item:
|
|
81
|
+
base, step_text = item.split("/", 1)
|
|
82
|
+
else:
|
|
83
|
+
base, step_text = item, None
|
|
84
|
+
step = int(step_text) if step_text is not None else 1
|
|
85
|
+
if step <= 0:
|
|
86
|
+
raise ValueError("cron step must be positive")
|
|
87
|
+
if base == "*":
|
|
88
|
+
start, end = minimum, maximum
|
|
89
|
+
elif "-" in base:
|
|
90
|
+
start_text, end_text = base.split("-", 1)
|
|
91
|
+
start, end = int(start_text), int(end_text)
|
|
92
|
+
else:
|
|
93
|
+
start = end = int(base)
|
|
94
|
+
if start < minimum or end > maximum or start > end:
|
|
95
|
+
raise ValueError("cron field is outside its valid range")
|
|
96
|
+
result.update(range(start, end + 1, step))
|
|
97
|
+
if not result:
|
|
98
|
+
raise ValueError("cron field must not be empty")
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = ["next_schedule_time"]
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Versioned Scheduler Lite source contracts (P2-06 foundation)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_camel(value: str) -> str:
|
|
14
|
+
head, *tail = value.split("_")
|
|
15
|
+
return head + "".join(part.capitalize() for part in tail)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ScheduleModel(BaseModel):
|
|
19
|
+
model_config = ConfigDict(
|
|
20
|
+
alias_generator=_to_camel,
|
|
21
|
+
populate_by_name=True,
|
|
22
|
+
extra="forbid",
|
|
23
|
+
frozen=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_TASK_ID = re.compile(r"^[a-z][a-z0-9-]{2,62}$")
|
|
28
|
+
ScheduleOccurrenceState = Literal[
|
|
29
|
+
"claimed",
|
|
30
|
+
"accepted",
|
|
31
|
+
"running",
|
|
32
|
+
"succeeded",
|
|
33
|
+
"failed",
|
|
34
|
+
"skipped",
|
|
35
|
+
"cancelled",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ScheduleSpec(ScheduleModel):
|
|
40
|
+
kind: Literal["once", "interval", "cron"]
|
|
41
|
+
timezone: str = "UTC"
|
|
42
|
+
at: datetime | None = None
|
|
43
|
+
every_seconds: int | None = Field(default=None, ge=60, le=31_536_000)
|
|
44
|
+
anchor_at: datetime | None = None
|
|
45
|
+
expression: str | None = Field(default=None, min_length=9, max_length=128)
|
|
46
|
+
misfire_policy: Literal["skip", "run_once"] = "skip"
|
|
47
|
+
|
|
48
|
+
@field_validator("timezone")
|
|
49
|
+
@classmethod
|
|
50
|
+
def validate_timezone(cls, value: str) -> str:
|
|
51
|
+
try:
|
|
52
|
+
ZoneInfo(value)
|
|
53
|
+
except ZoneInfoNotFoundError as error:
|
|
54
|
+
raise ValueError("timezone must be an IANA timezone") from error
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
@field_validator("at", "anchor_at")
|
|
58
|
+
@classmethod
|
|
59
|
+
def normalize_timestamp(cls, value: datetime | None) -> datetime | None:
|
|
60
|
+
if value is None:
|
|
61
|
+
return None
|
|
62
|
+
if value.tzinfo is None:
|
|
63
|
+
raise ValueError("schedule timestamps must include a timezone")
|
|
64
|
+
return value.astimezone(timezone.utc)
|
|
65
|
+
|
|
66
|
+
@model_validator(mode="after")
|
|
67
|
+
def validate_kind_shape(self) -> "ScheduleSpec":
|
|
68
|
+
if self.kind == "once":
|
|
69
|
+
if self.at is None or self.every_seconds is not None or self.expression is not None:
|
|
70
|
+
raise ValueError("once schedule requires only at")
|
|
71
|
+
elif self.kind == "interval":
|
|
72
|
+
if self.every_seconds is None or self.at is not None or self.expression is not None:
|
|
73
|
+
raise ValueError("interval schedule requires everySeconds and no expression")
|
|
74
|
+
elif self.kind == "cron":
|
|
75
|
+
if self.expression is None or self.at is not None or self.every_seconds is not None:
|
|
76
|
+
raise ValueError("cron schedule requires only expression")
|
|
77
|
+
_parse_cron(self.expression)
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ScheduledTaskTarget(ScheduleModel):
|
|
82
|
+
# ``agent_instance_id`` identifies the concrete Kernel owner which will
|
|
83
|
+
# consume an Inbox command. ``agent_id`` is the Studio-facing stable
|
|
84
|
+
# logical Agent identity used to retain a task on its detail page even when
|
|
85
|
+
# the task is pinned to an older immutable Build. It is optional only so
|
|
86
|
+
# existing v1 SQLite rows remain readable; all Studio-authored tasks set it.
|
|
87
|
+
agent_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
88
|
+
tenant_id: str = Field(min_length=1, max_length=256)
|
|
89
|
+
agent_instance_id: str = Field(min_length=1, max_length=256)
|
|
90
|
+
agent_version_ref: str | None = Field(default=None, min_length=1, max_length=256)
|
|
91
|
+
session_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
92
|
+
authorization_ref: str = Field(min_length=1, max_length=512)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ScheduleCommandTemplate(ScheduleModel):
|
|
96
|
+
"""The sole supported Scheduler Lite action: durable enqueue via AgentControl."""
|
|
97
|
+
|
|
98
|
+
command_type: Literal["enqueue"] = "enqueue"
|
|
99
|
+
payload: dict[str, Any]
|
|
100
|
+
|
|
101
|
+
@model_validator(mode="after")
|
|
102
|
+
def validate_payload(self) -> "ScheduleCommandTemplate":
|
|
103
|
+
if "content" not in self.payload:
|
|
104
|
+
raise ValueError("scheduled enqueue payload requires content")
|
|
105
|
+
_reject_clear_secrets(self.payload)
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ScheduledTask(ScheduleModel):
|
|
110
|
+
api_version: Literal["schedule.ksadk.io/v1"] = "schedule.ksadk.io/v1"
|
|
111
|
+
kind: Literal["ScheduledTask"] = "ScheduledTask"
|
|
112
|
+
schema_version: Literal[1] = 1
|
|
113
|
+
task_id: str = Field(min_length=3, max_length=63)
|
|
114
|
+
# A human-facing task label belongs to the durable schedule intent, not a
|
|
115
|
+
# transient Studio card. Optional keeps the previously frozen v1 rows
|
|
116
|
+
# and third-party producers readable during this additive transition.
|
|
117
|
+
display_name: str | None = Field(default=None, min_length=1, max_length=128)
|
|
118
|
+
target: ScheduledTaskTarget
|
|
119
|
+
schedule: ScheduleSpec
|
|
120
|
+
command: ScheduleCommandTemplate
|
|
121
|
+
enabled: bool = True
|
|
122
|
+
continuity: Literal["new_session", "continue_session"] = "new_session"
|
|
123
|
+
concurrency_policy: Literal["forbid"] = "forbid"
|
|
124
|
+
next_run_at: datetime | None = None
|
|
125
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
126
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
127
|
+
|
|
128
|
+
@field_validator("task_id")
|
|
129
|
+
@classmethod
|
|
130
|
+
def validate_task_id(cls, value: str) -> str:
|
|
131
|
+
if not _TASK_ID.fullmatch(value):
|
|
132
|
+
raise ValueError("taskId must be a lowercase stable identifier")
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
@model_validator(mode="after")
|
|
136
|
+
def validate_continuity_target(self) -> "ScheduledTask":
|
|
137
|
+
if self.continuity == "continue_session" and not self.target.session_id:
|
|
138
|
+
raise ValueError("continue_session schedule requires target.sessionId")
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
@field_validator("next_run_at", "created_at", "updated_at")
|
|
142
|
+
@classmethod
|
|
143
|
+
def normalize_timestamp(cls, value: datetime | None) -> datetime | None:
|
|
144
|
+
if value is None:
|
|
145
|
+
return None
|
|
146
|
+
if value.tzinfo is None:
|
|
147
|
+
raise ValueError("scheduler timestamps must include a timezone")
|
|
148
|
+
return value.astimezone(timezone.utc)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ScheduleOccurrenceTransition(ScheduleModel):
|
|
152
|
+
"""One durable state change displayed by Studio's execution history."""
|
|
153
|
+
|
|
154
|
+
state: ScheduleOccurrenceState
|
|
155
|
+
at: datetime
|
|
156
|
+
detail: str | None = Field(default=None, max_length=1024)
|
|
157
|
+
error_code: str | None = Field(default=None, max_length=128)
|
|
158
|
+
|
|
159
|
+
@field_validator("at")
|
|
160
|
+
@classmethod
|
|
161
|
+
def normalize_timestamp(cls, value: datetime) -> datetime:
|
|
162
|
+
if value.tzinfo is None:
|
|
163
|
+
raise ValueError("occurrence transition timestamps must include a timezone")
|
|
164
|
+
return value.astimezone(timezone.utc)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ScheduleOccurrence(ScheduleModel):
|
|
168
|
+
api_version: Literal["schedule.ksadk.io/v1"] = "schedule.ksadk.io/v1"
|
|
169
|
+
kind: Literal["ScheduleOccurrence"] = "ScheduleOccurrence"
|
|
170
|
+
schema_version: Literal[1] = 1
|
|
171
|
+
occurrence_id: str = Field(min_length=8, max_length=256)
|
|
172
|
+
task_id: str = Field(min_length=3, max_length=63)
|
|
173
|
+
# Keep the execution target as an immutable occurrence snapshot. Tasks can
|
|
174
|
+
# later be edited or deleted, but history/reconciliation must still know
|
|
175
|
+
# exactly which Agent revision accepted this occurrence.
|
|
176
|
+
target: ScheduledTaskTarget | None = None
|
|
177
|
+
scheduled_for: datetime
|
|
178
|
+
session_id: str = Field(min_length=1, max_length=256)
|
|
179
|
+
trigger: Literal["schedule", "manual"] = "schedule"
|
|
180
|
+
state: ScheduleOccurrenceState
|
|
181
|
+
attempt: int = Field(default=1, ge=1, le=100)
|
|
182
|
+
command_id: str | None = None
|
|
183
|
+
accepted_seq: int | None = Field(default=None, ge=0)
|
|
184
|
+
last_event_seq: int | None = Field(default=None, ge=0)
|
|
185
|
+
run_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
186
|
+
claimed_at: datetime | None = None
|
|
187
|
+
accepted_at: datetime | None = None
|
|
188
|
+
started_at: datetime | None = None
|
|
189
|
+
error_code: str | None = Field(default=None, max_length=128)
|
|
190
|
+
detail: str | None = Field(default=None, max_length=1024)
|
|
191
|
+
completed_at: datetime | None = None
|
|
192
|
+
transitions: tuple[ScheduleOccurrenceTransition, ...] = ()
|
|
193
|
+
|
|
194
|
+
@field_validator("scheduled_for", "claimed_at", "accepted_at", "started_at", "completed_at")
|
|
195
|
+
@classmethod
|
|
196
|
+
def normalize_timestamp(cls, value: datetime | None) -> datetime | None:
|
|
197
|
+
if value is None:
|
|
198
|
+
return None
|
|
199
|
+
if value.tzinfo is None:
|
|
200
|
+
raise ValueError("occurrence timestamps must include a timezone")
|
|
201
|
+
return value.astimezone(timezone.utc)
|
|
202
|
+
|
|
203
|
+
@model_validator(mode="after")
|
|
204
|
+
def validate_terminal_fields(self) -> "ScheduleOccurrence":
|
|
205
|
+
terminal = {"succeeded", "failed", "skipped", "cancelled"}
|
|
206
|
+
if self.state in terminal and self.completed_at is None:
|
|
207
|
+
raise ValueError("terminal occurrence requires completedAt")
|
|
208
|
+
if self.state == "failed" and not self.error_code:
|
|
209
|
+
raise ValueError("failed occurrence requires errorCode")
|
|
210
|
+
if self.state == "running" and (not self.run_id or self.started_at is None):
|
|
211
|
+
raise ValueError("running occurrence requires runId and startedAt")
|
|
212
|
+
if self.transitions:
|
|
213
|
+
timestamps = [transition.at for transition in self.transitions]
|
|
214
|
+
if timestamps != sorted(timestamps):
|
|
215
|
+
raise ValueError("occurrence transitions must be chronological")
|
|
216
|
+
if self.transitions[-1].state != self.state:
|
|
217
|
+
raise ValueError("last occurrence transition must match state")
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _reject_clear_secrets(value: Any, *, key: str = "payload") -> None:
|
|
222
|
+
if isinstance(value, dict):
|
|
223
|
+
for name, item in value.items():
|
|
224
|
+
lowered = str(name).lower()
|
|
225
|
+
if any(term in lowered for term in ("secret", "password", "token", "api_key")):
|
|
226
|
+
if not isinstance(item, str) or not item.startswith(
|
|
227
|
+
("env://", "secret://", "credential://", "vault://")
|
|
228
|
+
):
|
|
229
|
+
raise ValueError(f"{key}.{name} must be a secret reference, not a value")
|
|
230
|
+
_reject_clear_secrets(item, key=f"{key}.{name}")
|
|
231
|
+
elif isinstance(value, list):
|
|
232
|
+
for index, item in enumerate(value):
|
|
233
|
+
_reject_clear_secrets(item, key=f"{key}[{index}]")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _parse_cron(expression: str) -> tuple[str, str, str, str, str]:
|
|
237
|
+
fields = tuple(part for part in expression.split() if part)
|
|
238
|
+
if len(fields) != 5:
|
|
239
|
+
raise ValueError("cron expression must use exactly five fields")
|
|
240
|
+
for value in fields:
|
|
241
|
+
if not re.fullmatch(r"[0-9*/,-]+", value):
|
|
242
|
+
raise ValueError("cron fields only support numbers, ranges, lists and steps")
|
|
243
|
+
return fields # detailed field grammar is shared with calendar.py
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
__all__ = [
|
|
247
|
+
"ScheduleCommandTemplate",
|
|
248
|
+
"ScheduleOccurrence",
|
|
249
|
+
"ScheduleOccurrenceState",
|
|
250
|
+
"ScheduleOccurrenceTransition",
|
|
251
|
+
"ScheduleSpec",
|
|
252
|
+
"ScheduledTask",
|
|
253
|
+
"ScheduledTaskTarget",
|
|
254
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Local Scheduler Lite's explicit AgentControl dispatcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
7
|
+
|
|
8
|
+
from ksadk.kernel.contracts import (
|
|
9
|
+
AgentControlCommand,
|
|
10
|
+
AgentControlPermit,
|
|
11
|
+
AgentControlReceipt,
|
|
12
|
+
)
|
|
13
|
+
from ksadk.kernel.ingress import (
|
|
14
|
+
map_scheduler_request,
|
|
15
|
+
submit_command,
|
|
16
|
+
subscribe_projected,
|
|
17
|
+
trusted_context,
|
|
18
|
+
)
|
|
19
|
+
from ksadk.scheduler.contracts import ScheduledTask, ScheduledTaskTarget, ScheduleOccurrence
|
|
20
|
+
from ksadk.scheduler.engine import SchedulerDispatchError, SchedulerDispatchReceipt
|
|
21
|
+
|
|
22
|
+
SubmitCommand = Callable[[AgentControlCommand, AgentControlPermit], Awaitable[AgentControlReceipt]]
|
|
23
|
+
ReadOccurrenceEvents = Callable[
|
|
24
|
+
[ScheduleOccurrence], Awaitable[Sequence[tuple[int, object]]]
|
|
25
|
+
]
|
|
26
|
+
PrepareTarget = Callable[[ScheduledTaskTarget], Awaitable[object]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentControlSchedulerDispatcher:
|
|
30
|
+
"""Translate one occurrence into a single durable AgentControl enqueue.
|
|
31
|
+
|
|
32
|
+
This dispatcher is intentionally local-runtime only. A hosted scheduler
|
|
33
|
+
must obtain a Server-issued permit at its admission boundary in Phase 3;
|
|
34
|
+
this class never makes local permits look valid against hosted JWKS.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
submitter: SubmitCommand | None = None,
|
|
40
|
+
*,
|
|
41
|
+
event_reader: ReadOccurrenceEvents | None = None,
|
|
42
|
+
target_preparer: PrepareTarget | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._submitter = submitter or _submit_via_ingress
|
|
45
|
+
self._event_reader = event_reader
|
|
46
|
+
self._target_preparer = target_preparer
|
|
47
|
+
|
|
48
|
+
async def dispatch(
|
|
49
|
+
self, task: ScheduledTask, occurrence: ScheduleOccurrence
|
|
50
|
+
) -> SchedulerDispatchReceipt:
|
|
51
|
+
target = task.target
|
|
52
|
+
if self._target_preparer is not None:
|
|
53
|
+
await self._target_preparer(target)
|
|
54
|
+
trusted = trusted_context(
|
|
55
|
+
source_kind="scheduler",
|
|
56
|
+
source_ref=occurrence.occurrence_id,
|
|
57
|
+
tenant_id=target.tenant_id,
|
|
58
|
+
agent_instance_id=target.agent_instance_id,
|
|
59
|
+
session_id=occurrence.session_id,
|
|
60
|
+
operations=("enqueue",),
|
|
61
|
+
)
|
|
62
|
+
command = map_scheduler_request(
|
|
63
|
+
session_id=occurrence.session_id,
|
|
64
|
+
idempotency_key=occurrence.occurrence_id,
|
|
65
|
+
content=task.command.payload["content"],
|
|
66
|
+
occurrence_id=occurrence.occurrence_id,
|
|
67
|
+
trusted=trusted,
|
|
68
|
+
)
|
|
69
|
+
receipt = await self._submitter(command, trusted.permit)
|
|
70
|
+
if receipt.status not in {"accepted", "duplicate"}:
|
|
71
|
+
error = receipt.error
|
|
72
|
+
raise SchedulerDispatchError(
|
|
73
|
+
error.code if error else f"KERNEL_{receipt.status.upper()}",
|
|
74
|
+
error.message if error else f"kernel rejected occurrence: {receipt.status}",
|
|
75
|
+
)
|
|
76
|
+
return SchedulerDispatchReceipt(str(receipt.command_id), accepted_seq=receipt.accepted_seq)
|
|
77
|
+
|
|
78
|
+
async def read_events(self, occurrence: ScheduleOccurrence) -> tuple[tuple[int, object], ...]:
|
|
79
|
+
"""Read currently available canonical facts without holding an SSE open.
|
|
80
|
+
|
|
81
|
+
The local scheduler wakes frequently; each read resumes from the
|
|
82
|
+
durable session cursor persisted on the occurrence. This avoids a
|
|
83
|
+
hidden second event log and lets a Studio restart continue settling an
|
|
84
|
+
already-accepted run.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
if self._event_reader is not None:
|
|
88
|
+
return tuple(await self._event_reader(occurrence))
|
|
89
|
+
|
|
90
|
+
target = occurrence.target
|
|
91
|
+
if target is None:
|
|
92
|
+
return ()
|
|
93
|
+
trusted = trusted_context(
|
|
94
|
+
source_kind="scheduler",
|
|
95
|
+
source_ref=occurrence.occurrence_id,
|
|
96
|
+
tenant_id=target.tenant_id,
|
|
97
|
+
agent_instance_id=target.agent_instance_id,
|
|
98
|
+
session_id=occurrence.session_id,
|
|
99
|
+
operations=("subscribe_events",),
|
|
100
|
+
)
|
|
101
|
+
cursor = occurrence.last_event_seq
|
|
102
|
+
if cursor is None:
|
|
103
|
+
cursor = occurrence.accepted_seq or 0
|
|
104
|
+
stream = subscribe_projected(
|
|
105
|
+
occurrence.session_id,
|
|
106
|
+
trusted=trusted,
|
|
107
|
+
after_seq=cursor,
|
|
108
|
+
projector=lambda event: event,
|
|
109
|
+
)
|
|
110
|
+
events: list[tuple[int, object]] = []
|
|
111
|
+
try:
|
|
112
|
+
while len(events) < 100:
|
|
113
|
+
try:
|
|
114
|
+
value = await asyncio.wait_for(anext(stream), timeout=0.05)
|
|
115
|
+
except TimeoutError:
|
|
116
|
+
break
|
|
117
|
+
events.append(value)
|
|
118
|
+
finally:
|
|
119
|
+
await stream.aclose()
|
|
120
|
+
return tuple(events)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def _submit_via_ingress(
|
|
124
|
+
command: AgentControlCommand, permit: AgentControlPermit
|
|
125
|
+
) -> AgentControlReceipt:
|
|
126
|
+
return await submit_command(command, permit=permit)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = ["AgentControlSchedulerDispatcher"]
|