agentengine-sdk-python 0.7.0__py3-none-any.whl → 0.8.0__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.7.0.dist-info → agentengine_sdk_python-0.8.0.dist-info}/METADATA +36 -20
- agentengine_sdk_python-0.8.0.dist-info/RECORD +578 -0
- ksadk/a2a/__init__.py +178 -11
- ksadk/a2a/bootstrap.py +334 -0
- ksadk/a2a/card.py +120 -0
- ksadk/a2a/context_store.py +203 -0
- ksadk/a2a/control_plane.py +818 -0
- ksadk/a2a/credential.py +99 -0
- ksadk/a2a/event_adapter.py +149 -0
- ksadk/a2a/executor.py +406 -89
- ksadk/a2a/external_transport.py +516 -0
- ksadk/a2a/identity.py +304 -0
- ksadk/a2a/ids.py +22 -0
- ksadk/a2a/langgraph.py +285 -0
- ksadk/a2a/resume_store.py +201 -0
- ksadk/a2a/routes.py +105 -0
- ksadk/a2a/server.py +107 -60
- ksadk/a2a/space_client.py +1101 -0
- ksadk/a2a/task_adapter.py +423 -0
- ksadk/a2a/task_event_dispatcher.py +167 -0
- ksadk/a2a/task_event_outbox.py +326 -0
- ksadk/a2a/task_store.py +206 -0
- ksadk/a2ui/__init__.py +27 -0
- ksadk/a2ui/core.py +186 -0
- ksadk/a2ui/models.py +166 -0
- ksadk/a2ui/renderer.py +198 -0
- ksadk/agui/__init__.py +9 -0
- ksadk/agui/a2ui_projection.py +92 -0
- ksadk/agui/agent.py +857 -0
- ksadk/agui/config.py +86 -0
- ksadk/agui/routes.py +55 -0
- ksadk/api/client.py +233 -191
- ksadk/builders/__init__.py +2 -0
- ksadk/builders/base.py +16 -14
- ksadk/builders/code_builder.py +318 -236
- ksadk/builders/container_builder.py +187 -167
- ksadk/builders/framework_requirements.py +29 -11
- ksadk/builders/ks3_uploader.py +58 -27
- ksadk/builders/managed_runtime_builder.py +145 -0
- ksadk/builders/mcp_builder.py +91 -92
- ksadk/builders/requirements_utils.py +1 -5
- ksadk/cli/__init__.py +89 -171
- ksadk/cli/cmd_a2a.py +254 -70
- ksadk/cli/cmd_agent.py +19 -5
- ksadk/cli/cmd_build.py +158 -29
- ksadk/cli/cmd_completion.py +47 -35
- ksadk/cli/cmd_config.py +267 -198
- ksadk/cli/cmd_create.py +500 -720
- ksadk/cli/cmd_dashboard.py +120 -45
- ksadk/cli/cmd_deploy.py +117 -58
- ksadk/cli/cmd_destroy.py +44 -14
- ksadk/cli/cmd_files.py +63 -24
- ksadk/cli/cmd_hermes.py +168 -89
- ksadk/cli/cmd_invoke.py +184 -122
- ksadk/cli/cmd_launch.py +64 -28
- ksadk/cli/cmd_mcp.py +98 -64
- ksadk/cli/cmd_model.py +14 -5
- ksadk/cli/cmd_openclaw.py +483 -227
- ksadk/cli/cmd_replay.py +71 -0
- ksadk/cli/cmd_run.py +33 -10
- ksadk/cli/cmd_status.py +80 -31
- ksadk/cli/cmd_version.py +20 -13
- ksadk/cli/cmd_web.py +89 -13
- ksadk/cli/deploy_utils.py +20 -20
- ksadk/cli/dry_run.py +5 -3
- ksadk/cli/error_utils.py +37 -12
- ksadk/cli/global_options.py +1 -2
- ksadk/cli/local_runtime.py +1 -2
- ksadk/cli/model_catalog.py +6 -2
- ksadk/cli/network_options.py +34 -14
- ksadk/cli/resource_common.py +6 -2
- ksadk/cli/storage.py +0 -1
- ksadk/cli/ui.py +5 -3
- ksadk/cli/workflow_common.py +51 -17
- ksadk/codex/__init__.py +12 -0
- ksadk/codex/client.py +408 -0
- ksadk/codex/phase.py +143 -0
- ksadk/codex/runtime.py +449 -0
- ksadk/common/aicp_env.py +0 -1
- ksadk/common/auth.py +2 -2
- ksadk/common/constants.py +1 -3
- ksadk/common/llm_utils.py +17 -13
- ksadk/compat/__init__.py +5 -0
- ksadk/compat/adk_compat.py +200 -0
- ksadk/compat/copilotkit_a2ui.py +330 -0
- ksadk/configs/__init__.py +12 -12
- ksadk/configs/env_registry.py +500 -100
- ksadk/configs/global_config.py +2 -1
- ksadk/configs/settings.py +151 -124
- ksadk/conversations/__init__.py +7 -5
- ksadk/conversations/attachment_storage.py +16 -11
- ksadk/conversations/attachments.py +44 -36
- ksadk/conversations/compaction_pipeline.py +12 -12
- ksadk/conversations/compaction_prompt.py +9 -5
- ksadk/conversations/context.py +221 -6
- ksadk/conversations/message_projection.py +438 -51
- ksadk/conversations/model_context.py +12 -6
- ksadk/conversations/model_options.py +0 -1
- ksadk/conversations/normalize.py +20 -16
- ksadk/conversations/reasoning_markup.py +1 -1
- ksadk/conversations/runtime.py +339 -5255
- ksadk/conversations/runtime_compaction.py +333 -0
- ksadk/conversations/runtime_constants.py +27 -0
- ksadk/conversations/runtime_governance.py +185 -0
- ksadk/conversations/runtime_input.py +652 -0
- ksadk/conversations/runtime_invocation.py +395 -0
- ksadk/conversations/runtime_metadata.py +475 -0
- ksadk/conversations/runtime_observability.py +355 -0
- ksadk/conversations/runtime_payloads.py +272 -0
- ksadk/conversations/runtime_persistence.py +440 -0
- ksadk/conversations/runtime_preparation.py +418 -0
- ksadk/conversations/runtime_resume.py +816 -0
- ksadk/conversations/runtime_stream_events.py +902 -0
- ksadk/conversations/runtime_streaming.py +547 -0
- ksadk/conversations/semantic_summary.py +17 -5
- ksadk/conversations/session_title.py +6 -3
- ksadk/deployment/__init__.py +15 -16
- ksadk/deployment/agent_access.py +12 -23
- ksadk/deployment/base.py +51 -38
- ksadk/deployment/manager.py +63 -65
- ksadk/deployment/providers/serverless.py +309 -202
- ksadk/deployment/registry.py +38 -32
- ksadk/deployment/state.py +11 -11
- ksadk/deployment/ui_config.py +1 -1
- ksadk/detection/__init__.py +1 -1
- ksadk/detection/detector.py +146 -83
- ksadk/detection/mcp_detector.py +9 -6
- ksadk/events/__init__.py +25 -0
- ksadk/events/parser.py +191 -0
- ksadk/events/replay.py +37 -0
- ksadk/events/runtime_event.py +264 -0
- ksadk/events/store.py +317 -0
- ksadk/harness/__init__.py +34 -0
- ksadk/harness/app.py +278 -0
- ksadk/harness/config.py +181 -0
- ksadk/harness/reasoner.py +105 -0
- ksadk/harness/runner.py +211 -0
- ksadk/harness/sandbox.py +141 -0
- ksadk/harness/tools.py +222 -0
- ksadk/hermes_terminal.py +23 -12
- ksadk/identity/resolver.py +15 -9
- ksadk/ids.py +32 -0
- ksadk/knowledge_base/adk_tool.py +1 -1
- ksadk/knowledge_base/client.py +23 -29
- ksadk/managed_runtime.py +188 -0
- ksadk/markdown.py +5 -2
- ksadk/mcp_runtime/__init__.py +9 -6
- ksadk/memory/__init__.py +1 -1
- ksadk/memory/adk/__init__.py +2 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +2 -6
- ksadk/memory/adk/backends/http_ltm_backend.py +13 -21
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +7 -13
- ksadk/memory/adk/backends/sdk_ltm_backend.py +26 -33
- ksadk/memory/adk/long_term_memory.py +31 -35
- ksadk/memory/adk/resilient_session_service.py +4 -5
- ksadk/memory/adk/short_term_memory.py +12 -21
- ksadk/memory/adk_tool.py +3 -2
- ksadk/memory/backends/base.py +19 -19
- ksadk/memory/backends/memory.py +24 -25
- ksadk/memory/backends/redis.py +31 -30
- ksadk/memory/langchain_tool.py +2 -1
- ksadk/memory/manager.py +52 -47
- ksadk/memory/service.py +10 -8
- ksadk/model_policy.py +15 -7
- ksadk/model_proxy/__init__.py +51 -0
- ksadk/model_proxy/bootstrap.py +80 -0
- ksadk/model_proxy/cache.py +165 -0
- ksadk/model_proxy/config.py +45 -0
- ksadk/model_proxy/demo_webui.py +115 -0
- ksadk/model_proxy/detect.py +145 -0
- ksadk/model_proxy/gate.py +68 -0
- ksadk/model_proxy/namespace.py +173 -0
- ksadk/model_proxy/protocol_proxy.py +22 -0
- ksadk/model_proxy/server.py +300 -0
- ksadk/model_proxy/transform.py +691 -0
- ksadk/openclaw_gateway.py +29 -14
- ksadk/runners/adk_runner.py +345 -120
- ksadk/runners/base_runner.py +46 -29
- ksadk/runners/codex_runner.py +132 -0
- ksadk/runners/deepagents_runner.py +0 -1
- ksadk/runners/factory.py +17 -5
- ksadk/runners/langchain_runner.py +29 -742
- ksadk/runners/langgraph_runner.py +352 -90
- ksadk/runners/patch_langchain.py +16 -16
- ksadk/runners/remote_runner.py +134 -33
- ksadk/runners/usage_accumulator.py +1 -0
- ksadk/runners/utils/langfuse.py +3 -3
- ksadk/runners/utils/loader.py +7 -7
- ksadk/runtime/__init__.py +41 -0
- ksadk/runtime/adapter.py +345 -0
- ksadk/runtime/framework_adapters.py +124 -0
- ksadk/runtime/preprocessing.py +118 -0
- ksadk/runtime/runner_adapter.py +873 -0
- ksadk/runtime_context.py +18 -3
- ksadk/runtime_state.py +0 -1
- ksadk/sandbox/__init__.py +4 -1
- ksadk/sandbox/backends/__init__.py +4 -1
- ksadk/sandbox/backends/e2b.py +13 -5
- ksadk/sandbox/backends/local_process.py +11 -3
- ksadk/sandbox/base.py +10 -15
- ksadk/sandbox/factory.py +14 -4
- ksadk/sandbox/registry.py +57 -6
- ksadk/server/api_models.py +15 -5
- ksadk/server/app.py +455 -3885
- ksadk/server/composition.py +21 -0
- ksadk/server/factory.py +624 -0
- ksadk/server/routes/__init__.py +1 -0
- ksadk/server/routes/checkpoint_resolution.py +116 -0
- ksadk/server/routes/common.py +713 -0
- ksadk/server/routes/control.py +327 -0
- ksadk/server/routes/dependencies.py +98 -0
- ksadk/server/routes/misc.py +448 -0
- ksadk/server/routes/models.py +443 -0
- ksadk/server/routes/openai_compat.py +207 -0
- ksadk/server/routes/projection.py +795 -0
- ksadk/server/routes/routers.py +52 -0
- ksadk/server/routes/run.py +726 -0
- ksadk/server/routes/sessions.py +649 -0
- ksadk/server/routes/streaming.py +223 -0
- ksadk/server/routes/workspace.py +381 -0
- ksadk/server/static/assets/{ArtifactsPanel-D-SSn2ee.js → ArtifactsPanel-Cjnqt-8W.js} +1 -1
- ksadk/server/static/assets/CodeBlock-BqyqZfuB.js +9 -0
- ksadk/server/static/assets/MathMessageMarkdown-BpH7jiFM.js +8 -0
- ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +303 -0
- ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +1 -0
- ksadk/server/static/assets/{arc-BA13sHc9.js → arc-CFKwSk_B.js} +1 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-DzJzgVoL.js → architectureDiagram-3BPJPVTR-Dej8jX8c.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-GPEHLZMM-UTdVcZhn.js → blockDiagram-GPEHLZMM-1F64jqvy.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-AAUBKEIU-Dix0GD4m.js → c4Diagram-AAUBKEIU-9q7AUAKb.js} +1 -1
- ksadk/server/static/assets/channel-iUGv2N24.js +1 -0
- ksadk/server/static/assets/{chunk-2J33WTMH-C2JxuZ42.js → chunk-2J33WTMH-6qgwJm7Q.js} +1 -1
- ksadk/server/static/assets/{chunk-4BX2VUAB-Be57QPgD.js → chunk-4BX2VUAB-CsWIQRhc.js} +1 -1
- ksadk/server/static/assets/{chunk-55IACEB6-WjSuy4nv.js → chunk-55IACEB6-BK8paA20.js} +1 -1
- ksadk/server/static/assets/{chunk-727SXJPM-BOovP-b5.js → chunk-727SXJPM-B-JEhy7h.js} +1 -1
- ksadk/server/static/assets/{chunk-AQP2D5EJ-DOmZNmWj.js → chunk-AQP2D5EJ-DYBvMHAa.js} +1 -1
- ksadk/server/static/assets/{chunk-FMBD7UC4-DzwlodkU.js → chunk-FMBD7UC4-BGhvzDYt.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-DMqAikEU.js → chunk-ND2GUHAM-D98vPd-E.js} +1 -1
- ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-S5V4N54A-Zhy2GBnP.js → cose-bilkent-S5V4N54A-CX3yotHq.js} +1 -1
- ksadk/server/static/assets/{dagre-BM42HDAG-CbKrtIUz.js → dagre-BM42HDAG-C7TguAWo.js} +1 -1
- ksadk/server/static/assets/{diagram-2AECGRRQ-BRApgQRK.js → diagram-2AECGRRQ-Dd17_kws.js} +1 -1
- ksadk/server/static/assets/{diagram-5GNKFQAL-D0xbnDiK.js → diagram-5GNKFQAL-DVIClIil.js} +1 -1
- ksadk/server/static/assets/{diagram-KO2AKTUF-CVugVI9L.js → diagram-KO2AKTUF-Df_3gYQa.js} +1 -1
- ksadk/server/static/assets/{diagram-LMA3HP47-DozUGgee.js → diagram-LMA3HP47-BhLk6ZtA.js} +1 -1
- ksadk/server/static/assets/{diagram-OG6HWLK6-ZOYsKlru.js → diagram-OG6HWLK6-D4GNGvuu.js} +1 -1
- ksadk/server/static/assets/{dist-DmFaq_Iy.js → dist-6CVKuTAe.js} +1 -1
- ksadk/server/static/assets/{dist-8EEyGgDx.js → dist-BUHRxvDG.js} +1 -1
- ksadk/server/static/assets/{dist-B0NP_Z0z.js → dist-BZ9n9AjO.js} +1 -1
- ksadk/server/static/assets/{dist-DMezvBGJ.js → dist-BibcG1V9.js} +1 -1
- ksadk/server/static/assets/{dist-C3HIIaN5.js → dist-Bs-jQcRX.js} +1 -1
- ksadk/server/static/assets/{dist-BrCcQ47T.js → dist-C2xjE7cO.js} +1 -1
- ksadk/server/static/assets/{dist-Dp0k7bWI.js → dist-C7-3jInC.js} +1 -1
- ksadk/server/static/assets/{dist-D-xOD048.js → dist-C99THtIA.js} +1 -1
- ksadk/server/static/assets/{dist-CkEKJFtk.js → dist-CGcp2ex8.js} +1 -1
- ksadk/server/static/assets/{dist-DyOkZmze.js → dist-CVQRZjPu.js} +1 -1
- ksadk/server/static/assets/{dist-BssVmjIo.js → dist-Cf23NY4L.js} +1 -1
- ksadk/server/static/assets/{dist-BIJVDMuB.js → dist-CgF3fj15.js} +1 -1
- ksadk/server/static/assets/{dist-2hInLS_J.js → dist-Cwt7koer.js} +1 -1
- ksadk/server/static/assets/{dist-DyWsb78Q.js → dist-D82mtD2s.js} +1 -1
- ksadk/server/static/assets/{dist-D-JMdWPQ.js → dist-DF06ORQo.js} +1 -1
- ksadk/server/static/assets/{dist-BmlMPsCc.js → dist-DHn6nBzr.js} +1 -1
- ksadk/server/static/assets/{dist-gX6GOLKq.js → dist-Djgl2wVZ.js} +1 -1
- ksadk/server/static/assets/{dist-DpKtt4Bo.js → dist-DlzCRy0S.js} +1 -1
- ksadk/server/static/assets/{dist-8_SfK1IP.js → dist-DqNnNFrw.js} +1 -1
- ksadk/server/static/assets/{dist-D57wnGWd.js → dist-DyvYLMW9.js} +1 -1
- ksadk/server/static/assets/{dist-CPBtqRTW.js → dist-eoW6tZXA.js} +1 -1
- ksadk/server/static/assets/{dist-DL_NKc_z.js → dist-ivOya1LD.js} +1 -1
- ksadk/server/static/assets/{erDiagram-TEJ5UH35-Bk-9bgE5.js → erDiagram-TEJ5UH35-CnfRcY2M.js} +1 -1
- ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +1 -0
- ksadk/server/static/assets/{flowDiagram-I6XJVG4X-Dn9kZVuZ.js → flowDiagram-I6XJVG4X-DGaJjGNJ.js} +1 -1
- ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BGxXlJqm.js → ganttDiagram-6RSMTGT7-DpCoB2g9.js} +1 -1
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-C7Q0X-wi.js → gitGraphDiagram-PVQCEYII-9IKVFBVT.js} +1 -1
- ksadk/server/static/assets/index-QSwCHuB8.js +177 -0
- ksadk/server/static/assets/index-jOPmTOZH.css +2 -0
- ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +1 -0
- ksadk/server/static/assets/{infoDiagram-5YYISTIA-BO_CSqq5.js → infoDiagram-5YYISTIA-CYIIsb8y.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-eYLPLd46.js → ishikawaDiagram-YF4QCWOH-DLOHGvWw.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-De9geSb0.js → journeyDiagram-JHISSGLW-BIzKeejT.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-Cs80kQto.js → kanban-definition-UN3LZRKU-BWHoFSw2.js} +1 -1
- ksadk/server/static/assets/{katex-DSrCFD8n.js → katex-DATssjp1.js} +1 -1
- ksadk/server/static/assets/{linear-CN9sbKTF.js → linear-CvMBw9cA.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-DlgSBU8l.js → mermaid-parser.core-mGAcEwcq.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-9afz7Oug.js → mindmap-definition-RKZ34NQL-CSAQxEAe.js} +1 -1
- ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +1 -0
- ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +1 -0
- ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPv_VstN.js → pieDiagram-4H26LBE5-By-ra07M.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-o77wZ1XK.js → quadrantDiagram-W4KKPZXB-CxIR4JXI.js} +1 -1
- ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +1 -0
- ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-BYVw81VC.js → requirementDiagram-4Y6WPE33-DpW90iu3.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-B7nZFMKT.js → sankeyDiagram-5OEKKPKP-DpGa6EAV.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-BGRRrFw5.js → sequenceDiagram-3UESZ5HK-NTKNGGe5.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-AJRCARHV-BUfHCoPs.js → stateDiagram-AJRCARHV-DGpd9yFG.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +1 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-FbCTGrf9.js → timeline-definition-PNZ67QCA-jqoYs3jQ.js} +1 -1
- ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +1 -0
- ksadk/server/static/assets/{vennDiagram-CIIHVFJN-DWgwtPsJ.js → vennDiagram-CIIHVFJN-l3Z7-wbP.js} +1 -1
- ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-2x8G-tTM.js → wardleyDiagram-YWT4CUSO-BR54GPmr.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-Drma3AYl.js → xychartDiagram-2RQKCTM6-SkiH8GsK.js} +1 -1
- ksadk/server/static/index.html +2 -2
- ksadk/server/terminal_sessions.py +111 -58
- ksadk/sessions/__init__.py +69 -19
- ksadk/sessions/base.py +31 -11
- ksadk/sessions/continuity.py +2 -3
- ksadk/sessions/in_memory.py +46 -2
- ksadk/sessions/local_service.py +148 -17
- ksadk/sessions/postgres_service.py +143 -25
- ksadk/sessions/resilient.py +36 -8
- ksadk/skills/models.py +17 -5
- ksadk/skills/package_store.py +5 -2
- ksadk/skills/runtime/agent.py +20 -5
- ksadk/skills/runtime/artifacts.py +1 -5
- ksadk/skills/runtime/backends/e2b.py +10 -4
- ksadk/skills/runtime/backends/local.py +15 -3
- ksadk/skills/runtime/base.py +1 -2
- ksadk/skills/runtime/executor.py +10 -3
- ksadk/skills/runtime/factory.py +4 -2
- ksadk/skills/runtime/loader.py +1 -2
- ksadk/skills/runtime/registry.py +15 -10
- ksadk/skills/runtime/request.py +3 -1
- ksadk/skills/service_client.py +38 -17
- ksadk/skills/tool_defs.py +15 -10
- ksadk/terminal_exec_policy.py +3 -4
- ksadk/tools/gateway.py +92 -15
- ksadk/tools/result_budget.py +12 -4
- ksadk/toolsets/__init__.py +134 -36
- ksadk/toolsets/platform.py +21 -5
- ksadk/toolsets/sandbox.py +82 -16
- ksadk/toolsets/skills.py +200 -39
- ksadk/toolsets/web.py +123 -26
- ksadk/toolsets/workspace.py +212 -101
- ksadk/tracing/exporters/inmemory_exporter.py +62 -51
- ksadk/tracing/exporters/langfuse_exporter.py +91 -76
- ksadk/tracing/setup.py +69 -53
- ksadk/tracing/span_utils.py +5 -2
- ksadk/tui/loop.py +78 -30
- ksadk/tui/stream_render.py +1 -0
- ksadk/ui_config.py +5 -3
- ksadk/version.py +1 -1
- ksadk_runtime_common/schemas/runtime_event_v1.json +95 -0
- agentengine_sdk_python-0.7.0.dist-info/RECORD +0 -482
- ksadk/server/static/assets/CodeBlock-P16VOLYi.js +0 -9
- ksadk/server/static/assets/MathMessageMarkdown-CdGiQWHR.js +0 -8
- ksadk/server/static/assets/MermaidBlock-IOcal8cm.js +0 -303
- ksadk/server/static/assets/NativeTerminalPanel-DGSDWsei.js +0 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-BvUlsA6l.js +0 -1
- ksadk/server/static/assets/channel-CTuydARX.js +0 -1
- ksadk/server/static/assets/chunk-QZHKN3VN-BnCydivS.js +0 -1
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-BNHg45Be.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-BNHg45Be.js +0 -1
- ksadk/server/static/assets/eventmodeling-FCH6USID-DsAgWnlf.js +0 -1
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BIao9Yjg.js +0 -1
- ksadk/server/static/assets/index-C00Lwg3L.js +0 -165
- ksadk/server/static/assets/index-DAURbCsP.css +0 -1
- ksadk/server/static/assets/info-J43DQDTF-CyipqGxU.js +0 -1
- ksadk/server/static/assets/packet-YPE3B663-CGkyRxlg.js +0 -1
- ksadk/server/static/assets/pie-LRSECV5Y-M0uWAf4n.js +0 -1
- ksadk/server/static/assets/radar-GUYGQ44K-Bdbm24-x.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-8xBN_gLM.js +0 -1
- ksadk/server/static/assets/treeView-BLDUP644-npMo0ak1.js +0 -1
- ksadk/server/static/assets/treemap-LRROVOQU-CHdzrrdo.js +0 -1
- ksadk/server/static/assets/wardley-L42UT6IY-BEQz7wyy.js +0 -1
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.0.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.0.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
"""Space-scoped A2A discovery and authorized data-plane calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import uuid
|
|
10
|
+
from collections.abc import AsyncIterator, Mapping
|
|
11
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from http.cookies import CookieError, SimpleCookie
|
|
15
|
+
from typing import Any
|
|
16
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
from a2a.client import ClientCallContext, ClientConfig, create_client
|
|
20
|
+
from a2a.types import (
|
|
21
|
+
AgentCard,
|
|
22
|
+
CancelTaskRequest,
|
|
23
|
+
GetTaskRequest,
|
|
24
|
+
Message,
|
|
25
|
+
Part,
|
|
26
|
+
Role,
|
|
27
|
+
SendMessageConfiguration,
|
|
28
|
+
SendMessageRequest,
|
|
29
|
+
SubscribeToTaskRequest,
|
|
30
|
+
TaskState,
|
|
31
|
+
)
|
|
32
|
+
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
33
|
+
|
|
34
|
+
from ksadk.a2a.control_plane import (
|
|
35
|
+
ENV_A2A_CONTROL_PLANE_URL,
|
|
36
|
+
A2AControlPlane,
|
|
37
|
+
A2ARouteInterface,
|
|
38
|
+
CredentialInjection,
|
|
39
|
+
DiscoveredAgent,
|
|
40
|
+
InternalA2AControlPlaneClient,
|
|
41
|
+
PreparedA2AOperation,
|
|
42
|
+
SpaceAgentPage,
|
|
43
|
+
)
|
|
44
|
+
from ksadk.a2a.event_adapter import A2AEventAdapter
|
|
45
|
+
from ksadk.a2a.external_transport import A2AExternalTransport
|
|
46
|
+
from ksadk.a2a.ids import require_a2a_resource_id
|
|
47
|
+
from ksadk.a2a.task_event_dispatcher import A2ATaskEventDispatcher
|
|
48
|
+
from ksadk.a2a.task_event_outbox import (
|
|
49
|
+
A2ATaskEventOutbox,
|
|
50
|
+
InMemoryA2ATaskEventOutbox,
|
|
51
|
+
SQLiteA2ATaskEventOutbox,
|
|
52
|
+
)
|
|
53
|
+
from ksadk.events.runtime_event import RuntimeEvent
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
ENV_A2A_SPACE_IDS = "KSADK_A2A_SPACE_IDS"
|
|
58
|
+
ENV_A2A_ENABLE_PUBLIC_EGRESS = "KSADK_A2A_ENABLE_PUBLIC_EGRESS"
|
|
59
|
+
|
|
60
|
+
ERR_PUBLIC_EGRESS_DISABLED = "A2A_PUBLIC_EGRESS_DISABLED"
|
|
61
|
+
MAX_A2A_MESSAGE_BYTES = 1024 * 1024
|
|
62
|
+
MAX_A2A_MESSAGE_PARTS = 64
|
|
63
|
+
MAX_A2A_MESSAGE_ID_LENGTH = 128
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class A2APlatformTask:
|
|
68
|
+
"""AgentEngine task locator with an optional latest remote A2A snapshot."""
|
|
69
|
+
|
|
70
|
+
id: str
|
|
71
|
+
remote_task: Any | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _utc_now() -> str:
|
|
75
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _canonical_proto(value: Any) -> dict[str, Any]:
|
|
79
|
+
return MessageToDict(value, preserving_proto_field_name=False)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _canonical_sha256(value: Any) -> str:
|
|
83
|
+
payload = _canonical_proto(value)
|
|
84
|
+
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
85
|
+
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _present_message_field(value: Any, field_name: str) -> Any | None:
|
|
89
|
+
has_field = getattr(value, "HasField", None)
|
|
90
|
+
if callable(has_field):
|
|
91
|
+
try:
|
|
92
|
+
if not has_field(field_name):
|
|
93
|
+
return None
|
|
94
|
+
except ValueError:
|
|
95
|
+
pass
|
|
96
|
+
return getattr(value, field_name, None)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class A2ASpaceClient:
|
|
100
|
+
"""Discovers Space members and performs permit-authorized A2A calls."""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
space_id: str,
|
|
105
|
+
backend: A2AControlPlane,
|
|
106
|
+
*,
|
|
107
|
+
egress_enabled: bool = False,
|
|
108
|
+
httpx_client: httpx.AsyncClient | None = None,
|
|
109
|
+
external_transport: A2AExternalTransport | None = None,
|
|
110
|
+
event_sink: Any | None = None,
|
|
111
|
+
event_outbox: A2ATaskEventOutbox | None = None,
|
|
112
|
+
event_dispatcher: A2ATaskEventDispatcher | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
require_a2a_resource_id(space_id, "a2a-space-", field_name="space_id")
|
|
115
|
+
if external_transport is not None and not isinstance(
|
|
116
|
+
external_transport, A2AExternalTransport
|
|
117
|
+
):
|
|
118
|
+
raise TypeError("external_transport must implement A2AExternalTransport")
|
|
119
|
+
self._space_id = space_id
|
|
120
|
+
self._backend = backend
|
|
121
|
+
self._egress_enabled = egress_enabled
|
|
122
|
+
self._httpx_client = httpx_client
|
|
123
|
+
self._external_transport = external_transport
|
|
124
|
+
self._event_sink = event_sink
|
|
125
|
+
if event_dispatcher is not None and event_outbox is not None:
|
|
126
|
+
raise ValueError("pass either event_dispatcher or event_outbox, not both")
|
|
127
|
+
self._owns_event_dispatcher = event_dispatcher is None
|
|
128
|
+
self._event_dispatcher = event_dispatcher or A2ATaskEventDispatcher(
|
|
129
|
+
event_outbox or InMemoryA2ATaskEventOutbox(),
|
|
130
|
+
backend,
|
|
131
|
+
)
|
|
132
|
+
self._event_adapter = A2AEventAdapter()
|
|
133
|
+
self._agents_by_id: dict[str, DiscoveredAgent] = {}
|
|
134
|
+
self._agents_by_task: dict[str, DiscoveredAgent] = {}
|
|
135
|
+
self._seq = 0
|
|
136
|
+
self._persisted_wire_events: set[str] = set()
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_env(
|
|
140
|
+
cls,
|
|
141
|
+
*,
|
|
142
|
+
space_id: str | None = None,
|
|
143
|
+
backend: A2AControlPlane | None = None,
|
|
144
|
+
httpx_client: httpx.AsyncClient | None = None,
|
|
145
|
+
external_transport: A2AExternalTransport | None = None,
|
|
146
|
+
egress_enabled: bool | None = None,
|
|
147
|
+
event_sink: Any | None = None,
|
|
148
|
+
event_outbox: A2ATaskEventOutbox | None = None,
|
|
149
|
+
event_dispatcher: A2ATaskEventDispatcher | None = None,
|
|
150
|
+
) -> "A2ASpaceClient":
|
|
151
|
+
selected_space_id = str(space_id or "").strip()
|
|
152
|
+
if selected_space_id:
|
|
153
|
+
require_a2a_resource_id(
|
|
154
|
+
selected_space_id,
|
|
155
|
+
"a2a-space-",
|
|
156
|
+
field_name="space_id",
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
raw_space_ids = str(os.getenv(ENV_A2A_SPACE_IDS) or "").strip()
|
|
160
|
+
if not raw_space_ids:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"missing {ENV_A2A_SPACE_IDS}; pass space_id or add the Runtime Agent "
|
|
163
|
+
"to an A2A Space first"
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
configured_space_ids = json.loads(raw_space_ids)
|
|
167
|
+
except json.JSONDecodeError as exc:
|
|
168
|
+
raise ValueError(f"{ENV_A2A_SPACE_IDS} must be a JSON string array") from exc
|
|
169
|
+
if not isinstance(configured_space_ids, list) or not configured_space_ids:
|
|
170
|
+
raise ValueError(f"{ENV_A2A_SPACE_IDS} must be a non-empty JSON string array")
|
|
171
|
+
if len(configured_space_ids) > 100:
|
|
172
|
+
raise ValueError(f"{ENV_A2A_SPACE_IDS} cannot contain more than 100 Space IDs")
|
|
173
|
+
normalized_space_ids: list[str] = []
|
|
174
|
+
for index, configured_space_id in enumerate(configured_space_ids):
|
|
175
|
+
if not isinstance(configured_space_id, str):
|
|
176
|
+
raise ValueError(f"{ENV_A2A_SPACE_IDS}[{index}] must be an A2A Space ID string")
|
|
177
|
+
normalized = configured_space_id.strip()
|
|
178
|
+
require_a2a_resource_id(
|
|
179
|
+
normalized,
|
|
180
|
+
"a2a-space-",
|
|
181
|
+
field_name=f"{ENV_A2A_SPACE_IDS}[{index}]",
|
|
182
|
+
)
|
|
183
|
+
normalized_space_ids.append(normalized)
|
|
184
|
+
if len(set(normalized_space_ids)) != len(normalized_space_ids):
|
|
185
|
+
raise ValueError(f"{ENV_A2A_SPACE_IDS} must not contain duplicate Space IDs")
|
|
186
|
+
if len(normalized_space_ids) != 1:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"{ENV_A2A_SPACE_IDS} contains multiple Space IDs; pass space_id explicitly"
|
|
189
|
+
)
|
|
190
|
+
selected_space_id = normalized_space_ids[0]
|
|
191
|
+
if backend is None:
|
|
192
|
+
control_plane_url = str(os.getenv(ENV_A2A_CONTROL_PLANE_URL) or "").strip()
|
|
193
|
+
if not control_plane_url:
|
|
194
|
+
raise ValueError(f"missing {ENV_A2A_CONTROL_PLANE_URL}")
|
|
195
|
+
backend = InternalA2AControlPlaneClient(
|
|
196
|
+
control_plane_url,
|
|
197
|
+
httpx_client=httpx_client,
|
|
198
|
+
)
|
|
199
|
+
if egress_enabled is None:
|
|
200
|
+
raw_egress = os.getenv(ENV_A2A_ENABLE_PUBLIC_EGRESS) or ""
|
|
201
|
+
egress_enabled = raw_egress.strip().lower() in {"1", "true", "yes", "on"}
|
|
202
|
+
if event_outbox is None and event_dispatcher is None:
|
|
203
|
+
event_outbox = SQLiteA2ATaskEventOutbox()
|
|
204
|
+
return cls(
|
|
205
|
+
selected_space_id,
|
|
206
|
+
backend,
|
|
207
|
+
egress_enabled=egress_enabled,
|
|
208
|
+
httpx_client=httpx_client,
|
|
209
|
+
external_transport=external_transport,
|
|
210
|
+
event_sink=event_sink,
|
|
211
|
+
event_outbox=event_outbox,
|
|
212
|
+
event_dispatcher=event_dispatcher,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
async def discover(
|
|
216
|
+
self,
|
|
217
|
+
prompt: str | None = None,
|
|
218
|
+
*,
|
|
219
|
+
skill: str | None = None,
|
|
220
|
+
include_blocked: bool = False,
|
|
221
|
+
) -> list[DiscoveredAgent]:
|
|
222
|
+
page = await self._backend.list_space_agents(
|
|
223
|
+
self._space_id,
|
|
224
|
+
prompt=prompt,
|
|
225
|
+
skill_id=skill,
|
|
226
|
+
include_blocked=include_blocked,
|
|
227
|
+
)
|
|
228
|
+
for agent in page.agents:
|
|
229
|
+
require_a2a_resource_id(
|
|
230
|
+
agent.agent_id,
|
|
231
|
+
"a2a-agent-",
|
|
232
|
+
field_name="DiscoveredAgent.agent_id",
|
|
233
|
+
)
|
|
234
|
+
require_a2a_resource_id(
|
|
235
|
+
agent.version_id,
|
|
236
|
+
"a2a-version-",
|
|
237
|
+
field_name="DiscoveredAgent.version_id",
|
|
238
|
+
)
|
|
239
|
+
self._agents_by_id[agent.agent_id] = agent
|
|
240
|
+
return page.agents
|
|
241
|
+
|
|
242
|
+
def _check_egress(self, agent: DiscoveredAgent) -> None:
|
|
243
|
+
if agent.route_kind == "external_public" and not self._egress_enabled:
|
|
244
|
+
raise PermissionError(
|
|
245
|
+
f"{ERR_PUBLIC_EGRESS_DISABLED}: external Agent {agent.agent_id} requires "
|
|
246
|
+
"Network.EnablePublicAccess"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def event_dispatcher(self) -> A2ATaskEventDispatcher:
|
|
251
|
+
"""Runtime-scoped task event dispatcher used by this client."""
|
|
252
|
+
|
|
253
|
+
return self._event_dispatcher
|
|
254
|
+
|
|
255
|
+
async def aclose(self, *, flush_timeout_seconds: float = 5.0) -> None:
|
|
256
|
+
"""Stop the dispatcher only when this standalone client created it."""
|
|
257
|
+
|
|
258
|
+
if self._owns_event_dispatcher:
|
|
259
|
+
await self._event_dispatcher.stop(flush_timeout_seconds=flush_timeout_seconds)
|
|
260
|
+
|
|
261
|
+
async def __aenter__(self) -> "A2ASpaceClient":
|
|
262
|
+
return self
|
|
263
|
+
|
|
264
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
265
|
+
await self.aclose()
|
|
266
|
+
|
|
267
|
+
async def _resolve_agent(self, agent_id: str) -> DiscoveredAgent:
|
|
268
|
+
agent = self._agents_by_id.get(agent_id)
|
|
269
|
+
if agent is None:
|
|
270
|
+
await self.discover()
|
|
271
|
+
agent = self._agents_by_id.get(agent_id)
|
|
272
|
+
if agent is None:
|
|
273
|
+
raise KeyError(f"Agent {agent_id!r} is not discoverable in Space {self._space_id}")
|
|
274
|
+
self._check_egress(agent)
|
|
275
|
+
return agent
|
|
276
|
+
|
|
277
|
+
async def send_message(
|
|
278
|
+
self,
|
|
279
|
+
agent_id: str,
|
|
280
|
+
message: str | Message,
|
|
281
|
+
*,
|
|
282
|
+
return_immediately: bool = False,
|
|
283
|
+
idempotency_token: str | None = None,
|
|
284
|
+
) -> A2APlatformTask:
|
|
285
|
+
agent = await self._resolve_agent(agent_id)
|
|
286
|
+
normalized = self._normalize_initial_message(message)
|
|
287
|
+
prepared = await self._backend.prepare_call(
|
|
288
|
+
space_id=self._space_id,
|
|
289
|
+
target_agent_id=agent.agent_id,
|
|
290
|
+
expected_version_id=agent.version_id,
|
|
291
|
+
message_id=normalized.message_id,
|
|
292
|
+
message_sha256=_canonical_sha256(normalized),
|
|
293
|
+
idempotency_token=idempotency_token or f"idem-{uuid.uuid4().hex}",
|
|
294
|
+
)
|
|
295
|
+
self._validate_prepared_target(agent, prepared)
|
|
296
|
+
handle = await self._send_prepared_message(
|
|
297
|
+
prepared,
|
|
298
|
+
agent,
|
|
299
|
+
normalized,
|
|
300
|
+
return_immediately=return_immediately,
|
|
301
|
+
)
|
|
302
|
+
self._agents_by_task[prepared.platform_task_id] = agent
|
|
303
|
+
await self._record_agent_for_task(prepared.platform_task_id, agent)
|
|
304
|
+
return handle
|
|
305
|
+
|
|
306
|
+
async def continue_task(
|
|
307
|
+
self,
|
|
308
|
+
task_id: str,
|
|
309
|
+
message: str | Message,
|
|
310
|
+
*,
|
|
311
|
+
return_immediately: bool = False,
|
|
312
|
+
idempotency_token: str | None = None,
|
|
313
|
+
) -> A2APlatformTask:
|
|
314
|
+
require_a2a_resource_id(task_id, "a2a-task-", field_name="task_id")
|
|
315
|
+
normalized = self._normalize_initial_message(message)
|
|
316
|
+
prepared = await self._backend.prepare_task_operation(
|
|
317
|
+
platform_task_id=task_id,
|
|
318
|
+
operation="send_message",
|
|
319
|
+
message_id=normalized.message_id,
|
|
320
|
+
message_sha256=_canonical_sha256(normalized),
|
|
321
|
+
idempotency_token=idempotency_token or f"idem-{uuid.uuid4().hex}",
|
|
322
|
+
)
|
|
323
|
+
self._validate_prepared_ids(prepared)
|
|
324
|
+
remote_task = self._require_remote_task(prepared)
|
|
325
|
+
normalized.task_id = remote_task.remote_task_id
|
|
326
|
+
if remote_task.remote_context_id:
|
|
327
|
+
normalized.context_id = remote_task.remote_context_id
|
|
328
|
+
agent = self._agent_from_prepared(prepared)
|
|
329
|
+
return await self._send_prepared_message(
|
|
330
|
+
prepared,
|
|
331
|
+
agent,
|
|
332
|
+
normalized,
|
|
333
|
+
return_immediately=return_immediately,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
async def _send_prepared_message(
|
|
337
|
+
self,
|
|
338
|
+
prepared: PreparedA2AOperation,
|
|
339
|
+
agent: DiscoveredAgent,
|
|
340
|
+
message: Message,
|
|
341
|
+
*,
|
|
342
|
+
return_immediately: bool,
|
|
343
|
+
) -> A2APlatformTask:
|
|
344
|
+
first_task = None
|
|
345
|
+
remote_task_id = prepared.remote_task.remote_task_id if prepared.remote_task else None
|
|
346
|
+
remote_context_id = prepared.remote_task.remote_context_id if prepared.remote_task else None
|
|
347
|
+
operation_instance_id = self._operation_instance_id(prepared)
|
|
348
|
+
async with self._operation_client(agent, prepared) as (client, context):
|
|
349
|
+
request = SendMessageRequest(
|
|
350
|
+
message=message,
|
|
351
|
+
configuration=SendMessageConfiguration(return_immediately=return_immediately),
|
|
352
|
+
)
|
|
353
|
+
wire_position = 0
|
|
354
|
+
async for response in client.send_message(request, context=context):
|
|
355
|
+
response_task = _present_message_field(response, "task")
|
|
356
|
+
if response_task is not None and str(getattr(response_task, "id", None) or ""):
|
|
357
|
+
first_task = first_task or response_task
|
|
358
|
+
observed_task_id = str(response_task.id)
|
|
359
|
+
observed_context_id = str(response_task.context_id or "") or None
|
|
360
|
+
if remote_task_id is None:
|
|
361
|
+
await self._bind_task(prepared.platform_task_id, response_task)
|
|
362
|
+
elif (remote_task_id, remote_context_id) != (
|
|
363
|
+
observed_task_id,
|
|
364
|
+
observed_context_id,
|
|
365
|
+
):
|
|
366
|
+
raise RuntimeError("A2A_REMOTE_BINDING_CONFLICT")
|
|
367
|
+
remote_task_id = observed_task_id
|
|
368
|
+
remote_context_id = observed_context_id
|
|
369
|
+
if remote_task_id is not None:
|
|
370
|
+
self._validate_remote_task_observation(
|
|
371
|
+
remote_task_id,
|
|
372
|
+
remote_context_id,
|
|
373
|
+
response,
|
|
374
|
+
)
|
|
375
|
+
await self._project_stream_item(
|
|
376
|
+
prepared.platform_task_id,
|
|
377
|
+
response,
|
|
378
|
+
agent,
|
|
379
|
+
wire_position=wire_position,
|
|
380
|
+
operation_instance_id=operation_instance_id,
|
|
381
|
+
)
|
|
382
|
+
wire_position += 1
|
|
383
|
+
if return_immediately and first_task is not None:
|
|
384
|
+
break
|
|
385
|
+
return A2APlatformTask(
|
|
386
|
+
id=prepared.platform_task_id,
|
|
387
|
+
remote_task=first_task,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
async def subscribe(self, task_id: str):
|
|
391
|
+
require_a2a_resource_id(task_id, "a2a-task-", field_name="task_id")
|
|
392
|
+
prepared = await self._backend.prepare_task_operation(
|
|
393
|
+
platform_task_id=task_id,
|
|
394
|
+
operation="subscribe_to_task",
|
|
395
|
+
)
|
|
396
|
+
self._validate_prepared_ids(prepared)
|
|
397
|
+
agent = self._agent_from_prepared(prepared)
|
|
398
|
+
async for item, _ in self._iter_subscription(prepared, agent):
|
|
399
|
+
yield item
|
|
400
|
+
|
|
401
|
+
async def _iter_subscription(
|
|
402
|
+
self,
|
|
403
|
+
prepared: PreparedA2AOperation,
|
|
404
|
+
agent: DiscoveredAgent,
|
|
405
|
+
):
|
|
406
|
+
remote_task = self._require_remote_task(prepared)
|
|
407
|
+
operation_instance_id = self._operation_instance_id(prepared)
|
|
408
|
+
async with self._operation_client(agent, prepared) as (client, context):
|
|
409
|
+
wire_position = 0
|
|
410
|
+
async for event in client.subscribe(
|
|
411
|
+
SubscribeToTaskRequest(id=remote_task.remote_task_id),
|
|
412
|
+
context=context,
|
|
413
|
+
):
|
|
414
|
+
self._validate_remote_task_observation(
|
|
415
|
+
remote_task.remote_task_id,
|
|
416
|
+
remote_task.remote_context_id,
|
|
417
|
+
event,
|
|
418
|
+
)
|
|
419
|
+
persisted = await self._project_stream_item(
|
|
420
|
+
prepared.platform_task_id,
|
|
421
|
+
event,
|
|
422
|
+
agent,
|
|
423
|
+
wire_position=wire_position,
|
|
424
|
+
operation_instance_id=operation_instance_id,
|
|
425
|
+
)
|
|
426
|
+
wire_position += 1
|
|
427
|
+
yield event, persisted
|
|
428
|
+
|
|
429
|
+
async def cancel(
|
|
430
|
+
self,
|
|
431
|
+
task_id: str,
|
|
432
|
+
*,
|
|
433
|
+
idempotency_token: str | None = None,
|
|
434
|
+
) -> A2APlatformTask:
|
|
435
|
+
require_a2a_resource_id(task_id, "a2a-task-", field_name="task_id")
|
|
436
|
+
prepared = await self._backend.prepare_task_operation(
|
|
437
|
+
platform_task_id=task_id,
|
|
438
|
+
operation="cancel_task",
|
|
439
|
+
idempotency_token=idempotency_token or f"idem-{uuid.uuid4().hex}",
|
|
440
|
+
)
|
|
441
|
+
self._validate_prepared_ids(prepared)
|
|
442
|
+
remote_task_ref = self._require_remote_task(prepared)
|
|
443
|
+
agent = self._agent_from_prepared(prepared)
|
|
444
|
+
async with self._operation_client(agent, prepared) as (client, context):
|
|
445
|
+
remote_task = await client.cancel_task(
|
|
446
|
+
CancelTaskRequest(id=remote_task_ref.remote_task_id), context=context
|
|
447
|
+
)
|
|
448
|
+
self._validate_remote_task_observation(
|
|
449
|
+
remote_task_ref.remote_task_id,
|
|
450
|
+
remote_task_ref.remote_context_id,
|
|
451
|
+
remote_task,
|
|
452
|
+
)
|
|
453
|
+
await self._project_stream_item(
|
|
454
|
+
task_id,
|
|
455
|
+
remote_task,
|
|
456
|
+
agent,
|
|
457
|
+
wire_position=0,
|
|
458
|
+
operation_instance_id=self._operation_instance_id(prepared),
|
|
459
|
+
)
|
|
460
|
+
return A2APlatformTask(
|
|
461
|
+
id=task_id,
|
|
462
|
+
remote_task=remote_task,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
async def get_task(self, task_id: str) -> A2APlatformTask:
|
|
466
|
+
require_a2a_resource_id(task_id, "a2a-task-", field_name="task_id")
|
|
467
|
+
prepared = await self._backend.prepare_task_operation(
|
|
468
|
+
platform_task_id=task_id,
|
|
469
|
+
operation="get_task",
|
|
470
|
+
)
|
|
471
|
+
self._validate_prepared_ids(prepared)
|
|
472
|
+
remote_task_ref = self._require_remote_task(prepared)
|
|
473
|
+
agent = self._agent_from_prepared(prepared)
|
|
474
|
+
async with self._operation_client(agent, prepared) as (client, context):
|
|
475
|
+
remote_task = await client.get_task(
|
|
476
|
+
GetTaskRequest(id=remote_task_ref.remote_task_id), context=context
|
|
477
|
+
)
|
|
478
|
+
self._validate_remote_task_observation(
|
|
479
|
+
remote_task_ref.remote_task_id,
|
|
480
|
+
remote_task_ref.remote_context_id,
|
|
481
|
+
remote_task,
|
|
482
|
+
)
|
|
483
|
+
await self._project_stream_item(
|
|
484
|
+
task_id,
|
|
485
|
+
remote_task,
|
|
486
|
+
agent,
|
|
487
|
+
wire_position=0,
|
|
488
|
+
operation_instance_id=self._operation_instance_id(prepared),
|
|
489
|
+
)
|
|
490
|
+
return A2APlatformTask(
|
|
491
|
+
id=task_id,
|
|
492
|
+
remote_task=remote_task,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def _normalize_initial_message(self, message: str | Message) -> Message:
|
|
496
|
+
if isinstance(message, str):
|
|
497
|
+
message = Message(
|
|
498
|
+
role=Role.ROLE_USER,
|
|
499
|
+
parts=[Part(text=message)],
|
|
500
|
+
message_id=f"message-{uuid.uuid4().hex}",
|
|
501
|
+
)
|
|
502
|
+
if getattr(message, "task_id", "") or getattr(message, "context_id", ""):
|
|
503
|
+
raise ValueError("caller must not provide remote task_id/context_id")
|
|
504
|
+
if message.role != Role.ROLE_USER:
|
|
505
|
+
raise ValueError("A2A Message role must be user")
|
|
506
|
+
if not 1 <= len(message.parts) <= MAX_A2A_MESSAGE_PARTS:
|
|
507
|
+
raise ValueError("A2A Message parts must contain 1-64 items")
|
|
508
|
+
if not getattr(message, "message_id", ""):
|
|
509
|
+
message.message_id = f"message-{uuid.uuid4().hex}"
|
|
510
|
+
if len(message.message_id) > MAX_A2A_MESSAGE_ID_LENGTH:
|
|
511
|
+
raise ValueError("A2A Message message_id must contain 1-128 characters")
|
|
512
|
+
encoded = json.dumps(
|
|
513
|
+
_canonical_proto(message),
|
|
514
|
+
ensure_ascii=False,
|
|
515
|
+
sort_keys=True,
|
|
516
|
+
separators=(",", ":"),
|
|
517
|
+
).encode("utf-8")
|
|
518
|
+
if len(encoded) > MAX_A2A_MESSAGE_BYTES:
|
|
519
|
+
raise ValueError("A2A Message canonical JSON exceeds 1 MiB")
|
|
520
|
+
return message
|
|
521
|
+
|
|
522
|
+
@staticmethod
|
|
523
|
+
def _validate_prepared_target(
|
|
524
|
+
agent: DiscoveredAgent,
|
|
525
|
+
prepared: PreparedA2AOperation,
|
|
526
|
+
) -> None:
|
|
527
|
+
A2ASpaceClient._validate_prepared_ids(prepared)
|
|
528
|
+
if prepared.target.agent_id != agent.agent_id:
|
|
529
|
+
raise RuntimeError("PrepareA2ACall returned a different target Agent")
|
|
530
|
+
if prepared.target.version_id != agent.version_id:
|
|
531
|
+
raise RuntimeError("PrepareA2ACall returned a different target version")
|
|
532
|
+
if agent.card_sha256 and prepared.target.card_sha256 != agent.card_sha256:
|
|
533
|
+
raise RuntimeError("PrepareA2ACall returned a different AgentCard hash")
|
|
534
|
+
|
|
535
|
+
@staticmethod
|
|
536
|
+
def _validate_prepared_ids(prepared: PreparedA2AOperation) -> None:
|
|
537
|
+
require_a2a_resource_id(
|
|
538
|
+
prepared.platform_task_id,
|
|
539
|
+
"a2a-task-",
|
|
540
|
+
field_name="PreparedA2AOperation.platform_task_id",
|
|
541
|
+
)
|
|
542
|
+
require_a2a_resource_id(
|
|
543
|
+
prepared.target.agent_id,
|
|
544
|
+
"a2a-agent-",
|
|
545
|
+
field_name="PreparedA2AOperation.target.agent_id",
|
|
546
|
+
)
|
|
547
|
+
require_a2a_resource_id(
|
|
548
|
+
prepared.target.version_id,
|
|
549
|
+
"a2a-version-",
|
|
550
|
+
field_name="PreparedA2AOperation.target.version_id",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
@staticmethod
|
|
554
|
+
def _require_remote_task(prepared: PreparedA2AOperation):
|
|
555
|
+
remote_task = prepared.remote_task
|
|
556
|
+
if remote_task is None or not remote_task.remote_task_id:
|
|
557
|
+
raise RuntimeError("A2A_REMOTE_TASK_NOT_BOUND")
|
|
558
|
+
return remote_task
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _operation_instance_id(prepared: PreparedA2AOperation) -> str:
|
|
562
|
+
return hashlib.sha256(prepared.call_permit.encode("utf-8")).hexdigest()
|
|
563
|
+
|
|
564
|
+
@staticmethod
|
|
565
|
+
def _validate_remote_task_observation(
|
|
566
|
+
expected_task_id: str,
|
|
567
|
+
expected_context_id: str | None,
|
|
568
|
+
item: Any,
|
|
569
|
+
) -> None:
|
|
570
|
+
task = _present_message_field(item, "task")
|
|
571
|
+
if task is None and hasattr(item, "status") and hasattr(item, "id"):
|
|
572
|
+
task = item
|
|
573
|
+
status_update = _present_message_field(item, "status_update")
|
|
574
|
+
artifact_update = _present_message_field(item, "artifact_update")
|
|
575
|
+
message = _present_message_field(item, "message")
|
|
576
|
+
candidate = task or status_update or artifact_update or message
|
|
577
|
+
if candidate is None:
|
|
578
|
+
return
|
|
579
|
+
observed_task_id = str(
|
|
580
|
+
getattr(candidate, "id", "") or getattr(candidate, "task_id", "") or ""
|
|
581
|
+
)
|
|
582
|
+
observed_context_id = str(getattr(candidate, "context_id", "") or "") or None
|
|
583
|
+
if observed_task_id and observed_task_id != expected_task_id:
|
|
584
|
+
raise RuntimeError("A2A_REMOTE_BINDING_CONFLICT")
|
|
585
|
+
if observed_context_id != expected_context_id:
|
|
586
|
+
raise RuntimeError("A2A_REMOTE_BINDING_CONFLICT")
|
|
587
|
+
|
|
588
|
+
def _agent_from_prepared(self, prepared: PreparedA2AOperation) -> DiscoveredAgent:
|
|
589
|
+
cached = self._agents_by_id.get(prepared.target.agent_id)
|
|
590
|
+
if cached is not None and cached.version_id == prepared.target.version_id:
|
|
591
|
+
return cached
|
|
592
|
+
card = self._route_only_card(prepared.target.agent_id, prepared.target.version_id)
|
|
593
|
+
return DiscoveredAgent(
|
|
594
|
+
agent_id=prepared.target.agent_id,
|
|
595
|
+
version_id=prepared.target.version_id,
|
|
596
|
+
source="hosted" if prepared.route.kind == "hosted_gateway" else "external",
|
|
597
|
+
agent_card=card,
|
|
598
|
+
card_sha256=prepared.target.card_sha256,
|
|
599
|
+
route_kind=prepared.route.kind,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
def _route_only_card(self, name: str, version: str) -> AgentCard:
|
|
603
|
+
return ParseDict(
|
|
604
|
+
{
|
|
605
|
+
"name": name,
|
|
606
|
+
"description": "AgentEngine prepared A2A route",
|
|
607
|
+
"version": version,
|
|
608
|
+
"supportedInterfaces": [],
|
|
609
|
+
"capabilities": {},
|
|
610
|
+
"defaultInputModes": ["text/plain"],
|
|
611
|
+
"defaultOutputModes": ["text/plain"],
|
|
612
|
+
"skills": [],
|
|
613
|
+
},
|
|
614
|
+
AgentCard(),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
@asynccontextmanager
|
|
618
|
+
async def _operation_client(
|
|
619
|
+
self,
|
|
620
|
+
agent: DiscoveredAgent,
|
|
621
|
+
prepared: PreparedA2AOperation,
|
|
622
|
+
) -> AsyncIterator[tuple[Any, ClientCallContext]]:
|
|
623
|
+
# Product bootstrap starts the shared dispatcher during lifespan startup.
|
|
624
|
+
# Standalone/from_env clients own their dispatcher and must start it here.
|
|
625
|
+
await self._event_dispatcher.start()
|
|
626
|
+
injection = CredentialInjection()
|
|
627
|
+
headers: dict[str, str]
|
|
628
|
+
async with AsyncExitStack() as exit_stack:
|
|
629
|
+
if prepared.route.kind == "hosted_gateway":
|
|
630
|
+
headers = {
|
|
631
|
+
"Authorization": f"Bearer {self._backend.gateway_token()}",
|
|
632
|
+
"X-AgentEngine-A2A-Permit": prepared.call_permit,
|
|
633
|
+
}
|
|
634
|
+
http = self._httpx_client
|
|
635
|
+
else:
|
|
636
|
+
if prepared.route.kind == "external_public" and not self._egress_enabled:
|
|
637
|
+
raise PermissionError(ERR_PUBLIC_EGRESS_DISABLED)
|
|
638
|
+
if self._external_transport is None:
|
|
639
|
+
raise RuntimeError(
|
|
640
|
+
"A2A_EGRESS_TRANSPORT_REQUIRED: external calls require a Runtime network "
|
|
641
|
+
"guard transport"
|
|
642
|
+
)
|
|
643
|
+
lease = await exit_stack.enter_async_context(
|
|
644
|
+
self._external_transport.open_for_route(
|
|
645
|
+
prepared.route.interface,
|
|
646
|
+
route_kind=prepared.route.kind,
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
http = lease.httpx_client
|
|
650
|
+
injection = await self._backend.resolve_credential(
|
|
651
|
+
platform_task_id=prepared.platform_task_id,
|
|
652
|
+
credential_handle=prepared.credential_handle,
|
|
653
|
+
call_permit=prepared.call_permit,
|
|
654
|
+
)
|
|
655
|
+
headers = dict(injection.headers)
|
|
656
|
+
if injection.cookies:
|
|
657
|
+
if any(name.lower() == "cookie" for name in headers):
|
|
658
|
+
raise RuntimeError(
|
|
659
|
+
"A2A_CREDENTIAL_INJECTION_CONFLICT: Cookie header and cookie injection "
|
|
660
|
+
"cannot both be present"
|
|
661
|
+
)
|
|
662
|
+
headers["Cookie"] = self._cookie_header(injection.cookies)
|
|
663
|
+
route = prepared.route.interface
|
|
664
|
+
if injection.query:
|
|
665
|
+
route = A2ARouteInterface(
|
|
666
|
+
url=self._url_with_query(route.url, injection.query),
|
|
667
|
+
protocol_binding=route.protocol_binding,
|
|
668
|
+
protocol_version=route.protocol_version,
|
|
669
|
+
)
|
|
670
|
+
route_card = self._card_for_route(agent.agent_card, route)
|
|
671
|
+
owned_http = None
|
|
672
|
+
if http is None:
|
|
673
|
+
owned_http = httpx.AsyncClient(trust_env=False)
|
|
674
|
+
http = owned_http
|
|
675
|
+
client = await create_client(
|
|
676
|
+
agent=route_card,
|
|
677
|
+
client_config=ClientConfig(httpx_client=http, streaming=True),
|
|
678
|
+
)
|
|
679
|
+
try:
|
|
680
|
+
yield client, ClientCallContext(service_parameters=headers or None)
|
|
681
|
+
finally:
|
|
682
|
+
if owned_http is not None:
|
|
683
|
+
await client.close()
|
|
684
|
+
await owned_http.aclose()
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def _url_with_query(url: str, query: dict[str, str]) -> str:
|
|
688
|
+
parsed = urlsplit(url)
|
|
689
|
+
values = parse_qsl(parsed.query, keep_blank_values=True)
|
|
690
|
+
existing = {name for name, _ in values}
|
|
691
|
+
collision = existing.intersection(query)
|
|
692
|
+
if collision:
|
|
693
|
+
raise RuntimeError(
|
|
694
|
+
"A2A_CREDENTIAL_INJECTION_CONFLICT: credential query collides with route query: "
|
|
695
|
+
f"{sorted(collision)}"
|
|
696
|
+
)
|
|
697
|
+
values.extend(query.items())
|
|
698
|
+
return urlunsplit(
|
|
699
|
+
(parsed.scheme, parsed.netloc, parsed.path, urlencode(values), parsed.fragment)
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
@staticmethod
|
|
703
|
+
def _cookie_header(cookies: dict[str, str]) -> str:
|
|
704
|
+
jar = SimpleCookie()
|
|
705
|
+
try:
|
|
706
|
+
for name, value in cookies.items():
|
|
707
|
+
jar[name] = value
|
|
708
|
+
except CookieError as exc:
|
|
709
|
+
raise RuntimeError(
|
|
710
|
+
"A2A_CREDENTIAL_INJECTION_CONFLICT: invalid credential cookie"
|
|
711
|
+
) from exc
|
|
712
|
+
return jar.output(header="", sep="; ").strip()
|
|
713
|
+
|
|
714
|
+
@staticmethod
|
|
715
|
+
def _card_for_route(card: AgentCard, route: A2ARouteInterface) -> AgentCard:
|
|
716
|
+
payload = _canonical_proto(card)
|
|
717
|
+
payload["supportedInterfaces"] = [
|
|
718
|
+
{
|
|
719
|
+
"url": route.url,
|
|
720
|
+
"protocolBinding": route.protocol_binding,
|
|
721
|
+
"protocolVersion": route.protocol_version,
|
|
722
|
+
}
|
|
723
|
+
]
|
|
724
|
+
return ParseDict(payload, AgentCard())
|
|
725
|
+
|
|
726
|
+
async def _bind_task(self, platform_task_id: str, remote_task: Any) -> None:
|
|
727
|
+
await self._backend.bind_remote_task(
|
|
728
|
+
platform_task_id=platform_task_id,
|
|
729
|
+
remote_task_id=str(remote_task.id),
|
|
730
|
+
remote_context_id=str(remote_task.context_id or "") or None,
|
|
731
|
+
observed_at=_utc_now(),
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
async def _project_stream_item(
|
|
735
|
+
self,
|
|
736
|
+
platform_task_id: str,
|
|
737
|
+
item: Any,
|
|
738
|
+
agent: DiscoveredAgent,
|
|
739
|
+
*,
|
|
740
|
+
wire_position: int,
|
|
741
|
+
operation_instance_id: str,
|
|
742
|
+
) -> list[RuntimeEvent]:
|
|
743
|
+
runtime_events = self._stream_item_to_events(
|
|
744
|
+
item,
|
|
745
|
+
agent,
|
|
746
|
+
wire_position=wire_position,
|
|
747
|
+
invocation_id=platform_task_id,
|
|
748
|
+
)
|
|
749
|
+
platform_events = self._platform_events(
|
|
750
|
+
item,
|
|
751
|
+
platform_task_id,
|
|
752
|
+
operation_instance_id=operation_instance_id,
|
|
753
|
+
wire_position=wire_position,
|
|
754
|
+
)
|
|
755
|
+
if platform_events:
|
|
756
|
+
await self._event_dispatcher.enqueue(
|
|
757
|
+
platform_task_id=platform_task_id,
|
|
758
|
+
events=platform_events,
|
|
759
|
+
)
|
|
760
|
+
return await self._persist_events(runtime_events)
|
|
761
|
+
|
|
762
|
+
def _platform_events(
|
|
763
|
+
self,
|
|
764
|
+
item: Any,
|
|
765
|
+
platform_task_id: str,
|
|
766
|
+
*,
|
|
767
|
+
operation_instance_id: str,
|
|
768
|
+
wire_position: int,
|
|
769
|
+
) -> list[dict[str, Any]]:
|
|
770
|
+
events: list[dict[str, Any]] = []
|
|
771
|
+
|
|
772
|
+
def append_event(
|
|
773
|
+
kind: str,
|
|
774
|
+
payload: dict[str, Any],
|
|
775
|
+
*,
|
|
776
|
+
status: str | None = None,
|
|
777
|
+
occurred_at: str | None = None,
|
|
778
|
+
) -> None:
|
|
779
|
+
events.append(
|
|
780
|
+
self._platform_event(
|
|
781
|
+
kind,
|
|
782
|
+
payload,
|
|
783
|
+
platform_task_id,
|
|
784
|
+
operation_instance_id=operation_instance_id,
|
|
785
|
+
wire_position=wire_position,
|
|
786
|
+
event_ordinal=len(events),
|
|
787
|
+
status=status,
|
|
788
|
+
occurred_at=occurred_at,
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
task = _present_message_field(item, "task")
|
|
793
|
+
if task is None and hasattr(item, "status") and hasattr(item, "id"):
|
|
794
|
+
task = item
|
|
795
|
+
status_update = _present_message_field(item, "status_update")
|
|
796
|
+
artifact_update = _present_message_field(item, "artifact_update")
|
|
797
|
+
message = _present_message_field(item, "message")
|
|
798
|
+
if task is not None and getattr(task, "status", None) is not None:
|
|
799
|
+
payload = _canonical_proto(task.status)
|
|
800
|
+
state_name = TaskState.Name(task.status.state)
|
|
801
|
+
append_event(
|
|
802
|
+
"status",
|
|
803
|
+
payload,
|
|
804
|
+
status=state_name.removeprefix("TASK_STATE_").lower(),
|
|
805
|
+
occurred_at=str(payload.get("timestamp") or _utc_now()),
|
|
806
|
+
)
|
|
807
|
+
for artifact in getattr(task, "artifacts", None) or []:
|
|
808
|
+
append_event(
|
|
809
|
+
"artifact",
|
|
810
|
+
{
|
|
811
|
+
"Artifact": _canonical_proto(artifact),
|
|
812
|
+
"Append": False,
|
|
813
|
+
"LastChunk": True,
|
|
814
|
+
},
|
|
815
|
+
)
|
|
816
|
+
if status_update is not None and getattr(status_update, "status", None) is not None:
|
|
817
|
+
payload = _canonical_proto(status_update.status)
|
|
818
|
+
state_name = TaskState.Name(status_update.status.state)
|
|
819
|
+
append_event(
|
|
820
|
+
"status",
|
|
821
|
+
payload,
|
|
822
|
+
status=state_name.removeprefix("TASK_STATE_").lower(),
|
|
823
|
+
occurred_at=str(payload.get("timestamp") or _utc_now()),
|
|
824
|
+
)
|
|
825
|
+
if artifact_update is not None and getattr(artifact_update, "artifact", None) is not None:
|
|
826
|
+
append_event(
|
|
827
|
+
"artifact",
|
|
828
|
+
{
|
|
829
|
+
"Artifact": _canonical_proto(artifact_update.artifact),
|
|
830
|
+
"Append": bool(getattr(artifact_update, "append", False)),
|
|
831
|
+
"LastChunk": bool(getattr(artifact_update, "last_chunk", False)),
|
|
832
|
+
},
|
|
833
|
+
)
|
|
834
|
+
if message is not None:
|
|
835
|
+
payload = _canonical_proto(message)
|
|
836
|
+
append_event("message", payload)
|
|
837
|
+
append_event(
|
|
838
|
+
"status",
|
|
839
|
+
{"state": "TASK_STATE_COMPLETED", "message": payload},
|
|
840
|
+
status="completed",
|
|
841
|
+
)
|
|
842
|
+
return events
|
|
843
|
+
|
|
844
|
+
@staticmethod
|
|
845
|
+
def _platform_event(
|
|
846
|
+
kind: str,
|
|
847
|
+
payload: dict[str, Any],
|
|
848
|
+
platform_task_id: str,
|
|
849
|
+
*,
|
|
850
|
+
operation_instance_id: str,
|
|
851
|
+
wire_position: int,
|
|
852
|
+
event_ordinal: int,
|
|
853
|
+
status: str | None = None,
|
|
854
|
+
occurred_at: str | None = None,
|
|
855
|
+
) -> dict[str, Any]:
|
|
856
|
+
source_id = hashlib.sha256(
|
|
857
|
+
(
|
|
858
|
+
f"{platform_task_id}:{operation_instance_id}:{wire_position}:{event_ordinal}:{kind}"
|
|
859
|
+
).encode("utf-8")
|
|
860
|
+
).hexdigest()
|
|
861
|
+
event: dict[str, Any] = {
|
|
862
|
+
"SourceEventId": source_id,
|
|
863
|
+
"EventKind": kind,
|
|
864
|
+
"Payload": payload,
|
|
865
|
+
"OccurredAt": occurred_at or _utc_now(),
|
|
866
|
+
}
|
|
867
|
+
if status:
|
|
868
|
+
event["Status"] = status
|
|
869
|
+
return event
|
|
870
|
+
|
|
871
|
+
async def flush_pending_events(self) -> int:
|
|
872
|
+
"""Deliver all currently queued platform event batches or raise on failure."""
|
|
873
|
+
|
|
874
|
+
return await self._event_dispatcher.drain(raise_on_error=True)
|
|
875
|
+
|
|
876
|
+
def _next_seq(self) -> int:
|
|
877
|
+
self._seq += 1
|
|
878
|
+
return self._seq
|
|
879
|
+
|
|
880
|
+
def _event_ctx(
|
|
881
|
+
self,
|
|
882
|
+
agent: DiscoveredAgent,
|
|
883
|
+
invocation_id: str,
|
|
884
|
+
*,
|
|
885
|
+
event_id: str | None = None,
|
|
886
|
+
) -> dict[str, Any]:
|
|
887
|
+
return {
|
|
888
|
+
"agent_id": agent.agent_id,
|
|
889
|
+
"user_id": "a2a_space",
|
|
890
|
+
"session_id": self._space_id,
|
|
891
|
+
"invocation_id": invocation_id,
|
|
892
|
+
"seq_id": self._next_seq(),
|
|
893
|
+
"event_id": event_id,
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
def task_to_event(self, task: Any, agent: DiscoveredAgent) -> RuntimeEvent:
|
|
897
|
+
return self._event_adapter.task_status_to_event(
|
|
898
|
+
task.status, **self._event_ctx(agent, invocation_id=str(task.id))
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
def _stream_item_to_events(
|
|
902
|
+
self,
|
|
903
|
+
item: Any,
|
|
904
|
+
agent: DiscoveredAgent,
|
|
905
|
+
*,
|
|
906
|
+
wire_position: int = 0,
|
|
907
|
+
invocation_id: str | None = None,
|
|
908
|
+
) -> list[RuntimeEvent]:
|
|
909
|
+
events: list[RuntimeEvent] = []
|
|
910
|
+
task = _present_message_field(item, "task")
|
|
911
|
+
if task is None and hasattr(item, "status") and hasattr(item, "id"):
|
|
912
|
+
task = item
|
|
913
|
+
status_update = _present_message_field(item, "status_update")
|
|
914
|
+
artifact_update = _present_message_field(item, "artifact_update")
|
|
915
|
+
message = _present_message_field(item, "message")
|
|
916
|
+
resolved_invocation_id = invocation_id or str(
|
|
917
|
+
getattr(item, "task_id", None)
|
|
918
|
+
or getattr(task, "id", "")
|
|
919
|
+
or getattr(status_update, "task_id", "")
|
|
920
|
+
or getattr(artifact_update, "task_id", "")
|
|
921
|
+
or getattr(message, "task_id", "")
|
|
922
|
+
or ""
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
def ctx(kind: str, value: Any) -> dict[str, Any]:
|
|
926
|
+
metadata = getattr(value, "metadata", None)
|
|
927
|
+
native_event_id = ""
|
|
928
|
+
if metadata is not None:
|
|
929
|
+
if isinstance(metadata, Mapping):
|
|
930
|
+
metadata_dict = dict(metadata)
|
|
931
|
+
else:
|
|
932
|
+
try:
|
|
933
|
+
metadata_dict = MessageToDict(metadata, preserving_proto_field_name=True)
|
|
934
|
+
except (AttributeError, TypeError, ValueError):
|
|
935
|
+
metadata_dict = {}
|
|
936
|
+
native_event_id = str(
|
|
937
|
+
metadata_dict.get("event_id") or metadata_dict.get("ksadk_event_id") or ""
|
|
938
|
+
)
|
|
939
|
+
message_id = str(getattr(value, "message_id", "") or "")
|
|
940
|
+
artifact = getattr(value, "artifact", None)
|
|
941
|
+
artifact_id = str(
|
|
942
|
+
getattr(value, "artifact_id", "") or getattr(artifact, "artifact_id", "") or ""
|
|
943
|
+
)
|
|
944
|
+
source_id = native_event_id or message_id or artifact_id
|
|
945
|
+
event_id = uuid.uuid5(
|
|
946
|
+
uuid.NAMESPACE_URL,
|
|
947
|
+
f"ksadk:a2a:{resolved_invocation_id}:{wire_position}:{kind}:{source_id}",
|
|
948
|
+
).hex
|
|
949
|
+
return self._event_ctx(agent, invocation_id=resolved_invocation_id, event_id=event_id)
|
|
950
|
+
|
|
951
|
+
if task is not None and getattr(task, "status", None) is not None:
|
|
952
|
+
task_status_message = _present_message_field(task.status, "message")
|
|
953
|
+
task_status_text = A2AEventAdapter._parts_text(
|
|
954
|
+
getattr(task_status_message, "parts", None)
|
|
955
|
+
)
|
|
956
|
+
task_is_terminal = task.status.state in {
|
|
957
|
+
TaskState.TASK_STATE_COMPLETED,
|
|
958
|
+
TaskState.TASK_STATE_FAILED,
|
|
959
|
+
TaskState.TASK_STATE_CANCELED,
|
|
960
|
+
TaskState.TASK_STATE_REJECTED,
|
|
961
|
+
}
|
|
962
|
+
if not task_is_terminal:
|
|
963
|
+
events.append(
|
|
964
|
+
self._event_adapter.task_status_to_event(
|
|
965
|
+
task.status,
|
|
966
|
+
**ctx("task", task),
|
|
967
|
+
)
|
|
968
|
+
)
|
|
969
|
+
if task_status_text:
|
|
970
|
+
events.append(
|
|
971
|
+
self._event_adapter.message_to_event(
|
|
972
|
+
task_status_text,
|
|
973
|
+
final=task_is_terminal,
|
|
974
|
+
**ctx("task-status-message", task_status_message),
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
if task_is_terminal:
|
|
978
|
+
events.append(
|
|
979
|
+
self._event_adapter.task_status_to_event(
|
|
980
|
+
task.status,
|
|
981
|
+
**ctx("task", task),
|
|
982
|
+
)
|
|
983
|
+
)
|
|
984
|
+
if status_update is not None and getattr(status_update, "status", None) is not None:
|
|
985
|
+
status_message = _present_message_field(status_update.status, "message")
|
|
986
|
+
text = A2AEventAdapter._parts_text(getattr(status_message, "parts", None))
|
|
987
|
+
terminal_states = {
|
|
988
|
+
TaskState.TASK_STATE_COMPLETED,
|
|
989
|
+
TaskState.TASK_STATE_FAILED,
|
|
990
|
+
TaskState.TASK_STATE_CANCELED,
|
|
991
|
+
TaskState.TASK_STATE_REJECTED,
|
|
992
|
+
}
|
|
993
|
+
is_terminal = status_update.status.state in terminal_states
|
|
994
|
+
if not is_terminal:
|
|
995
|
+
events.append(
|
|
996
|
+
self._event_adapter.task_status_to_event(
|
|
997
|
+
status_update.status, **ctx("status", status_update)
|
|
998
|
+
)
|
|
999
|
+
)
|
|
1000
|
+
if text:
|
|
1001
|
+
events.append(
|
|
1002
|
+
self._event_adapter.message_to_event(
|
|
1003
|
+
text,
|
|
1004
|
+
final=is_terminal,
|
|
1005
|
+
**ctx("status-message", status_message),
|
|
1006
|
+
)
|
|
1007
|
+
)
|
|
1008
|
+
if is_terminal:
|
|
1009
|
+
events.append(
|
|
1010
|
+
self._event_adapter.task_status_to_event(
|
|
1011
|
+
status_update.status, **ctx("status", status_update)
|
|
1012
|
+
)
|
|
1013
|
+
)
|
|
1014
|
+
if artifact_update is not None and getattr(artifact_update, "artifact", None) is not None:
|
|
1015
|
+
artifact = artifact_update.artifact
|
|
1016
|
+
events.append(
|
|
1017
|
+
self._event_adapter.artifact_to_event(artifact, **ctx("artifact", artifact_update))
|
|
1018
|
+
)
|
|
1019
|
+
artifact_text = A2AEventAdapter._parts_text(getattr(artifact, "parts", None))
|
|
1020
|
+
if artifact_text and str(getattr(artifact, "name", "") or "") == "response":
|
|
1021
|
+
events.append(
|
|
1022
|
+
self._event_adapter.message_to_event(
|
|
1023
|
+
artifact_text,
|
|
1024
|
+
final=bool(getattr(artifact_update, "last_chunk", False)),
|
|
1025
|
+
**ctx("artifact-text", artifact_update),
|
|
1026
|
+
)
|
|
1027
|
+
)
|
|
1028
|
+
if message is not None:
|
|
1029
|
+
text = A2AEventAdapter._parts_text(getattr(message, "parts", None))
|
|
1030
|
+
if text:
|
|
1031
|
+
events.append(
|
|
1032
|
+
self._event_adapter.message_to_event(
|
|
1033
|
+
text, final=True, **ctx("message", message)
|
|
1034
|
+
)
|
|
1035
|
+
)
|
|
1036
|
+
return events
|
|
1037
|
+
|
|
1038
|
+
async def _persist_events(self, events: list[RuntimeEvent]) -> list[RuntimeEvent]:
|
|
1039
|
+
existing_ids = set(self._persisted_wire_events)
|
|
1040
|
+
if self._event_sink is not None:
|
|
1041
|
+
list_events = getattr(self._event_sink, "list", None)
|
|
1042
|
+
if callable(list_events) and events:
|
|
1043
|
+
persisted_before = await list_events(events[0].session_id)
|
|
1044
|
+
existing_ids.update(event.event_id for event in persisted_before)
|
|
1045
|
+
fresh = [event for event in events if event.event_id not in existing_ids]
|
|
1046
|
+
if not fresh:
|
|
1047
|
+
return []
|
|
1048
|
+
if self._event_sink is not None:
|
|
1049
|
+
append = getattr(self._event_sink, "append", None)
|
|
1050
|
+
if append is None:
|
|
1051
|
+
raise TypeError("event_sink must provide async append(events)")
|
|
1052
|
+
persisted = await append(fresh)
|
|
1053
|
+
if persisted is not None:
|
|
1054
|
+
fresh = list(persisted)
|
|
1055
|
+
self._persisted_wire_events.update(event.event_id for event in fresh)
|
|
1056
|
+
return fresh
|
|
1057
|
+
|
|
1058
|
+
async def subscribe_events(self, task_id: str):
|
|
1059
|
+
require_a2a_resource_id(task_id, "a2a-task-", field_name="task_id")
|
|
1060
|
+
prepared = await self._backend.prepare_task_operation(
|
|
1061
|
+
platform_task_id=task_id,
|
|
1062
|
+
operation="subscribe_to_task",
|
|
1063
|
+
)
|
|
1064
|
+
self._validate_prepared_ids(prepared)
|
|
1065
|
+
agent = self._agent_from_prepared(prepared)
|
|
1066
|
+
async for _, persisted in self._iter_subscription(prepared, agent):
|
|
1067
|
+
for event in persisted:
|
|
1068
|
+
yield event
|
|
1069
|
+
|
|
1070
|
+
async def subscribe_persisted_events(
|
|
1071
|
+
self,
|
|
1072
|
+
*,
|
|
1073
|
+
after_seq_id: int = 0,
|
|
1074
|
+
timeout: float = 1.0,
|
|
1075
|
+
) -> AsyncIterator[RuntimeEvent]:
|
|
1076
|
+
subscribe = getattr(self._event_sink, "subscribe_session", None)
|
|
1077
|
+
if subscribe is None:
|
|
1078
|
+
raise RuntimeError("event_sink does not support subscribe_session cursor replay")
|
|
1079
|
+
async for event in subscribe(
|
|
1080
|
+
self._space_id,
|
|
1081
|
+
after_seq_id=after_seq_id,
|
|
1082
|
+
timeout=timeout,
|
|
1083
|
+
):
|
|
1084
|
+
yield event
|
|
1085
|
+
|
|
1086
|
+
async def _record_agent_for_task(self, task_id: str, agent: DiscoveredAgent) -> None:
|
|
1087
|
+
set_task_agent = getattr(self._event_sink, "set_task_agent", None)
|
|
1088
|
+
if callable(set_task_agent):
|
|
1089
|
+
await set_task_agent(self._space_id, task_id, agent.agent_id)
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
__all__ = [
|
|
1093
|
+
"A2AExternalTransport",
|
|
1094
|
+
"A2APlatformTask",
|
|
1095
|
+
"A2ASpaceClient",
|
|
1096
|
+
"DiscoveredAgent",
|
|
1097
|
+
"ENV_A2A_ENABLE_PUBLIC_EGRESS",
|
|
1098
|
+
"ENV_A2A_SPACE_IDS",
|
|
1099
|
+
"ERR_PUBLIC_EGRESS_DISABLED",
|
|
1100
|
+
"SpaceAgentPage",
|
|
1101
|
+
]
|