agentengine-sdk-python 0.7.0__py3-none-any.whl → 0.8.1__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.1.dist-info}/METADATA +52 -20
- agentengine_sdk_python-0.8.1.dist-info/RECORD +644 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/WHEEL +1 -1
- ksadk/a2a/__init__.py +180 -11
- ksadk/a2a/bootstrap.py +332 -0
- ksadk/a2a/card.py +15 -0
- ksadk/a2a/context_store.py +203 -0
- ksadk/a2a/control_plane.py +1056 -0
- ksadk/a2a/credential.py +99 -0
- ksadk/a2a/event_adapter.py +149 -0
- ksadk/a2a/executor.py +361 -137
- 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 +112 -0
- ksadk/a2a/server.py +102 -62
- ksadk/a2a/service_env.py +33 -0
- ksadk/a2a/space_client.py +1122 -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/a2a_card.py +101 -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 +872 -0
- ksadk/agui/config.py +92 -0
- ksadk/agui/routes.py +47 -0
- ksadk/api/client.py +238 -204
- ksadk/api/environment.py +27 -0
- ksadk/builders/__init__.py +2 -0
- ksadk/builders/base.py +16 -14
- ksadk/builders/code_builder.py +375 -270
- ksadk/builders/container_builder.py +240 -200
- ksadk/builders/framework_requirements.py +31 -12
- ksadk/builders/ks3_uploader.py +60 -29
- ksadk/builders/managed_runtime_builder.py +173 -0
- ksadk/builders/mcp_builder.py +91 -92
- ksadk/builders/requirements_utils.py +1 -5
- ksadk/cli/__init__.py +97 -171
- ksadk/cli/cmd_a2a.py +255 -84
- 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 +507 -740
- ksadk/cli/cmd_dashboard.py +120 -45
- ksadk/cli/cmd_deploy.py +135 -59
- ksadk/cli/cmd_destroy.py +44 -14
- ksadk/cli/cmd_eval.py +386 -0
- ksadk/cli/cmd_files.py +63 -24
- ksadk/cli/cmd_hermes.py +313 -135
- ksadk/cli/cmd_invoke.py +184 -122
- ksadk/cli/cmd_launch.py +76 -28
- ksadk/cli/cmd_mcp.py +98 -64
- ksadk/cli/cmd_model.py +14 -5
- ksadk/cli/cmd_openclaw.py +564 -268
- ksadk/cli/cmd_replay.py +71 -0
- ksadk/cli/cmd_run.py +58 -73
- ksadk/cli/cmd_status.py +80 -31
- ksadk/cli/cmd_studio.py +110 -0
- ksadk/cli/cmd_version.py +20 -13
- ksadk/cli/cmd_web.py +95 -18
- ksadk/cli/deploy_utils.py +20 -20
- ksadk/cli/dry_run.py +7 -4
- ksadk/cli/env_options.py +83 -0
- ksadk/cli/error_utils.py +37 -12
- ksadk/cli/global_options.py +4 -3
- ksadk/cli/local_runtime.py +1 -2
- ksadk/cli/model_catalog.py +6 -2
- ksadk/cli/network_options.py +41 -19
- ksadk/cli/resource_common.py +6 -2
- ksadk/cli/runtime_bootstrap.py +51 -0
- ksadk/cli/storage.py +0 -1
- ksadk/cli/ui.py +11 -3
- ksadk/cli/workflow_common.py +52 -18
- ksadk/codex/__init__.py +12 -0
- ksadk/codex/client.py +1042 -0
- ksadk/codex/phase.py +143 -0
- ksadk/codex/runtime.py +940 -0
- ksadk/common/aicp_env.py +0 -1
- ksadk/common/auth.py +2 -2
- ksadk/common/constants.py +1 -3
- ksadk/common/kop_client.py +232 -0
- 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 -14
- ksadk/configs/env_registry.py +661 -116
- ksadk/configs/global_config.py +3 -2
- ksadk/configs/settings.py +133 -209
- ksadk/conversations/__init__.py +85 -92
- 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 +660 -0
- ksadk/conversations/runtime_invocation.py +395 -0
- ksadk/conversations/runtime_metadata.py +475 -0
- ksadk/conversations/runtime_observability.py +367 -0
- ksadk/conversations/runtime_payloads.py +272 -0
- ksadk/conversations/runtime_persistence.py +440 -0
- ksadk/conversations/runtime_preparation.py +426 -0
- ksadk/conversations/runtime_resume.py +836 -0
- ksadk/conversations/runtime_stream_events.py +902 -0
- ksadk/conversations/runtime_streaming.py +700 -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/managed_runtime.py +33 -0
- ksadk/deployment/manager.py +85 -71
- ksadk/deployment/providers/serverless.py +379 -210
- 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/evaluation/__init__.py +84 -0
- ksadk/evaluation/a2a_adapter.py +444 -0
- ksadk/evaluation/adapters.py +54 -0
- ksadk/evaluation/contracts.py +409 -0
- ksadk/evaluation/evalset.py +351 -0
- ksadk/evaluation/evaluators.py +461 -0
- ksadk/evaluation/executor.py +118 -0
- ksadk/evaluation/storage.py +68 -0
- ksadk/evaluation/target.py +73 -0
- ksadk/events/__init__.py +25 -0
- ksadk/events/parser.py +191 -0
- ksadk/events/replay.py +37 -0
- ksadk/events/runtime_event.py +273 -0
- ksadk/events/store.py +317 -0
- ksadk/harness/__init__.py +35 -0
- ksadk/harness/app.py +269 -0
- ksadk/harness/config.py +181 -0
- ksadk/harness/reasoner.py +105 -0
- ksadk/harness/runtime.py +364 -0
- ksadk/harness/sandbox.py +141 -0
- ksadk/harness/tools.py +222 -0
- ksadk/hermes_terminal.py +28 -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 +38 -33
- ksadk/managed_a2a_card.py +160 -0
- ksadk/managed_runtime.py +195 -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 +36 -36
- ksadk/memory/adk/long_term_memory.py +34 -37
- 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 +16 -6
- ksadk/memory/tool.py +4 -0
- 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 +66 -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 +201 -0
- ksadk/model_proxy/protocol_proxy.py +22 -0
- ksadk/model_proxy/server.py +443 -0
- ksadk/model_proxy/transform.py +735 -0
- ksadk/openclaw_gateway.py +29 -14
- ksadk/runners/adk_runner.py +354 -146
- ksadk/runners/base_runner.py +71 -29
- ksadk/runners/deepagents_runner.py +0 -1
- ksadk/runners/factory.py +18 -5
- ksadk/runners/langchain_runner.py +29 -742
- ksadk/runners/langgraph_runner.py +417 -97
- ksadk/runners/patch_langchain.py +16 -16
- ksadk/runners/remote_runner.py +134 -33
- ksadk/runners/usage_accumulator.py +1 -0
- ksadk/runners/utils/__init__.py +0 -10
- ksadk/runners/utils/loader.py +23 -7
- ksadk/runtime/__init__.py +67 -0
- ksadk/runtime/adapter.py +412 -0
- ksadk/runtime/conversation_execution.py +553 -0
- ksadk/runtime/executor.py +251 -0
- ksadk/runtime/factory.py +156 -0
- ksadk/runtime/framework_adapters.py +118 -0
- ksadk/runtime/launch.py +48 -0
- ksadk/runtime/preprocessing.py +131 -0
- ksadk/runtime/runner_adapter.py +1032 -0
- ksadk/runtime/runner_loading.py +36 -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/__init__.py +39 -4
- ksadk/server/api_models.py +15 -5
- ksadk/server/app.py +24 -3928
- ksadk/server/composition.py +47 -0
- ksadk/server/factory.py +622 -0
- ksadk/server/routes/__init__.py +1 -0
- ksadk/server/routes/checkpoint_resolution.py +116 -0
- ksadk/server/routes/common.py +682 -0
- ksadk/server/routes/control.py +331 -0
- ksadk/server/routes/dependencies.py +98 -0
- ksadk/server/routes/misc.py +425 -0
- ksadk/server/routes/models.py +451 -0
- ksadk/server/routes/openai_compat.py +229 -0
- ksadk/server/routes/projection.py +848 -0
- ksadk/server/routes/routers.py +52 -0
- ksadk/server/routes/run.py +519 -0
- ksadk/server/routes/sessions.py +644 -0
- ksadk/server/routes/streaming.py +265 -0
- ksadk/server/routes/workspace.py +414 -0
- ksadk/server/static/assets/{ArtifactsPanel-D-SSn2ee.js → ArtifactsPanel-CakCqxEN.js} +1 -1
- ksadk/server/static/assets/CodeBlock-CynLknyO.js +9 -0
- ksadk/server/static/assets/MathMessageMarkdown-3sQdOysS.js +8 -0
- ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +303 -0
- ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +1 -0
- ksadk/server/static/assets/{arc-BA13sHc9.js → arc-DrgjcKhl.js} +1 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-DzJzgVoL.js → architectureDiagram-3BPJPVTR-BXoVRbQ6.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-GPEHLZMM-UTdVcZhn.js → blockDiagram-GPEHLZMM-BnaB3Ywv.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-AAUBKEIU-Dix0GD4m.js → c4Diagram-AAUBKEIU-Dw4weZcp.js} +1 -1
- ksadk/server/static/assets/channel-Dt3rQTFI.js +1 -0
- ksadk/server/static/assets/{chunk-2J33WTMH-C2JxuZ42.js → chunk-2J33WTMH-ChjpLK7j.js} +1 -1
- ksadk/server/static/assets/{chunk-4BX2VUAB-Be57QPgD.js → chunk-4BX2VUAB-B9drTgOW.js} +1 -1
- ksadk/server/static/assets/{chunk-55IACEB6-WjSuy4nv.js → chunk-55IACEB6-CK4ROATy.js} +1 -1
- ksadk/server/static/assets/{chunk-727SXJPM-BOovP-b5.js → chunk-727SXJPM-B9KtTTgw.js} +1 -1
- ksadk/server/static/assets/{chunk-AQP2D5EJ-DOmZNmWj.js → chunk-AQP2D5EJ-1O1mki3m.js} +1 -1
- ksadk/server/static/assets/{chunk-FMBD7UC4-DzwlodkU.js → chunk-FMBD7UC4-B_fH2YuH.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-DMqAikEU.js → chunk-ND2GUHAM-D6rFLaVV.js} +1 -1
- ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-S5V4N54A-Zhy2GBnP.js → cose-bilkent-S5V4N54A-ChNs_Mdk.js} +1 -1
- ksadk/server/static/assets/{dagre-BM42HDAG-CbKrtIUz.js → dagre-BM42HDAG-D0QXg_MQ.js} +1 -1
- ksadk/server/static/assets/{diagram-2AECGRRQ-BRApgQRK.js → diagram-2AECGRRQ-DBDDKwBQ.js} +1 -1
- ksadk/server/static/assets/{diagram-5GNKFQAL-D0xbnDiK.js → diagram-5GNKFQAL-BkwDOxmO.js} +1 -1
- ksadk/server/static/assets/{diagram-KO2AKTUF-CVugVI9L.js → diagram-KO2AKTUF-Bv8waYvY.js} +1 -1
- ksadk/server/static/assets/{diagram-LMA3HP47-DozUGgee.js → diagram-LMA3HP47-e1VECDdu.js} +1 -1
- ksadk/server/static/assets/{diagram-OG6HWLK6-ZOYsKlru.js → diagram-OG6HWLK6-Ds9adfRl.js} +1 -1
- ksadk/server/static/assets/{dist-DpKtt4Bo.js → dist-4N2NMVYg.js} +1 -1
- ksadk/server/static/assets/{dist-CkEKJFtk.js → dist-B-jNR6Vn.js} +1 -1
- ksadk/server/static/assets/{dist-D57wnGWd.js → dist-B5J2uhNk.js} +1 -1
- ksadk/server/static/assets/{dist-8_SfK1IP.js → dist-B5oS1zcj.js} +1 -1
- ksadk/server/static/assets/{dist-DMezvBGJ.js → dist-BB3-OwTQ.js} +1 -1
- ksadk/server/static/assets/{dist-2hInLS_J.js → dist-BIlKoW0k.js} +1 -1
- ksadk/server/static/assets/{dist-BrCcQ47T.js → dist-BjOdlDjj.js} +1 -1
- ksadk/server/static/assets/{dist-BmlMPsCc.js → dist-C1sz5Eo3.js} +1 -1
- ksadk/server/static/assets/{dist-B0NP_Z0z.js → dist-CWV4lDr1.js} +1 -1
- ksadk/server/static/assets/{dist-D-xOD048.js → dist-CmZHzlwZ.js} +1 -1
- ksadk/server/static/assets/{dist-DL_NKc_z.js → dist-D6MpCZtz.js} +1 -1
- ksadk/server/static/assets/{dist-DyOkZmze.js → dist-DJN_6Y39.js} +1 -1
- ksadk/server/static/assets/{dist-DmFaq_Iy.js → dist-DQU9EWw-.js} +1 -1
- ksadk/server/static/assets/{dist-gX6GOLKq.js → dist-DWKByxWV.js} +1 -1
- ksadk/server/static/assets/{dist-DyWsb78Q.js → dist-Dli7qW-B.js} +1 -1
- ksadk/server/static/assets/{dist-BIJVDMuB.js → dist-DsHPdk3W.js} +1 -1
- ksadk/server/static/assets/{dist-Dp0k7bWI.js → dist-X4TnqwOK.js} +1 -1
- ksadk/server/static/assets/{dist-8EEyGgDx.js → dist-_pHPTyZ-.js} +1 -1
- ksadk/server/static/assets/{dist-CPBtqRTW.js → dist-fhOuWds5.js} +1 -1
- ksadk/server/static/assets/{dist-D-JMdWPQ.js → dist-gLnrGsoU.js} +1 -1
- ksadk/server/static/assets/{dist-BssVmjIo.js → dist-j6Dxmf_j.js} +1 -1
- ksadk/server/static/assets/{dist-C3HIIaN5.js → dist-o2OVmKvt.js} +1 -1
- ksadk/server/static/assets/{erDiagram-TEJ5UH35-Bk-9bgE5.js → erDiagram-TEJ5UH35-BPItFKH6.js} +1 -1
- ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +1 -0
- ksadk/server/static/assets/{flowDiagram-I6XJVG4X-Dn9kZVuZ.js → flowDiagram-I6XJVG4X-BbYB9w5Y.js} +1 -1
- ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BGxXlJqm.js → ganttDiagram-6RSMTGT7-TQXa0KXv.js} +1 -1
- ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-C7Q0X-wi.js → gitGraphDiagram-PVQCEYII-bKblCnpa.js} +1 -1
- ksadk/server/static/assets/index-R52pJJvg.js +177 -0
- ksadk/server/static/assets/index-jJr3_955.css +2 -0
- ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +1 -0
- ksadk/server/static/assets/{infoDiagram-5YYISTIA-BO_CSqq5.js → infoDiagram-5YYISTIA-BGVT7J_i.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-eYLPLd46.js → ishikawaDiagram-YF4QCWOH-K0OOfVna.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-De9geSb0.js → journeyDiagram-JHISSGLW-8LeOJaZp.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-Cs80kQto.js → kanban-definition-UN3LZRKU-C6uCgZrW.js} +1 -1
- ksadk/server/static/assets/{katex-DSrCFD8n.js → katex-CYWseY5E.js} +1 -1
- ksadk/server/static/assets/{linear-CN9sbKTF.js → linear-hdBcYzM8.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-DlgSBU8l.js → mermaid-parser.core-BWrDjSZM.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-9afz7Oug.js → mindmap-definition-RKZ34NQL-Bjy_2v4_.js} +1 -1
- ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +1 -0
- ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +1 -0
- ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPv_VstN.js → pieDiagram-4H26LBE5-GmmqNiVH.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-o77wZ1XK.js → quadrantDiagram-W4KKPZXB-BmkRbwCg.js} +1 -1
- ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +1 -0
- ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-BYVw81VC.js → requirementDiagram-4Y6WPE33-DSWFqL7c.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-B7nZFMKT.js → sankeyDiagram-5OEKKPKP-l6iXmU65.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-BGRRrFw5.js → sequenceDiagram-3UESZ5HK-Cq6UBeQB.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-AJRCARHV-BUfHCoPs.js → stateDiagram-AJRCARHV-qzTQpg3Y.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +1 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-FbCTGrf9.js → timeline-definition-PNZ67QCA-DEz24vV0.js} +1 -1
- ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +1 -0
- ksadk/server/static/assets/{vennDiagram-CIIHVFJN-DWgwtPsJ.js → vennDiagram-CIIHVFJN-CMCixki2.js} +1 -1
- ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-2x8G-tTM.js → wardleyDiagram-YWT4CUSO-CVzB8F9K.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-Drma3AYl.js → xychartDiagram-2RQKCTM6-BFBqBLc1.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 +47 -16
- ksadk/skills/tool_defs.py +15 -10
- ksadk/studio/__init__.py +5 -0
- ksadk/studio/agent_avatar_assets.py +120 -0
- ksadk/studio/agent_lifecycle.py +51 -0
- ksadk/studio/api.py +1046 -0
- ksadk/studio/api_catalog_routes.py +324 -0
- ksadk/studio/api_contracts.py +177 -0
- ksadk/studio/api_helpers.py +132 -0
- ksadk/studio/authoring.py +524 -0
- ksadk/studio/authoring_coordinator.py +285 -0
- ksadk/studio/builder.py +320 -0
- ksadk/studio/capabilities.py +351 -0
- ksadk/studio/cloud.py +278 -0
- ksadk/studio/codex_agent_service.py +583 -0
- ksadk/studio/codex_builder.py +324 -0
- ksadk/studio/codex_manifest.py +278 -0
- ksadk/studio/codex_run.py +352 -0
- ksadk/studio/compiler.py +189 -0
- ksadk/studio/contracts.py +644 -0
- ksadk/studio/errors.py +47 -0
- ksadk/studio/evaluation.py +192 -0
- ksadk/studio/event_store.py +284 -0
- ksadk/studio/framework_run.py +114 -0
- ksadk/studio/identifiers.py +33 -0
- ksadk/studio/mcp_runtime.py +229 -0
- ksadk/studio/model_client.py +543 -0
- ksadk/studio/model_profile_service.py +214 -0
- ksadk/studio/operations.py +243 -0
- ksadk/studio/otel_trace.py +754 -0
- ksadk/studio/pagination.py +182 -0
- ksadk/studio/python_tool_inspection.py +183 -0
- ksadk/studio/repository.py +306 -0
- ksadk/studio/resource_catalog.py +1217 -0
- ksadk/studio/run_service.py +707 -0
- ksadk/studio/runtime_catalog.py +74 -0
- ksadk/studio/runtime_source.py +339 -0
- ksadk/studio/service.py +1141 -0
- ksadk/studio/shared_web.py +994 -0
- ksadk/studio/skill_discovery.py +628 -0
- ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
- ksadk/studio/static/assets/index-CaEN0eTR.css +1 -0
- ksadk/studio/static/assets/index-DI2eY_NH.js +252 -0
- ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
- ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- ksadk/studio/static/index.html +15 -0
- ksadk/studio/templates.py +630 -0
- ksadk/studio/validator.py +246 -0
- ksadk/studio/workspace.py +137 -0
- ksadk/terminal_exec_policy.py +3 -4
- ksadk/tools/gateway.py +101 -15
- ksadk/tools/result_budget.py +12 -4
- ksadk/toolsets/__init__.py +161 -37
- ksadk/toolsets/a2a.py +426 -0
- 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/__init__.py +19 -21
- ksadk/tracing/exporters/inmemory_exporter.py +62 -51
- ksadk/tracing/setup.py +135 -506
- ksadk/tracing/span_utils.py +0 -41
- 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/memory_backend/registry.py +56 -56
- ksadk_runtime_common/schemas/runtime_event_v1.json +95 -0
- ksadk_runtime_common/workspace_files/router.py +8 -2
- agentengine_sdk_python-0.7.0.dist-info/RECORD +0 -482
- ksadk/a2a/card_builder.py +0 -64
- ksadk/a2a/client.py +0 -309
- ksadk/runners/utils/langfuse.py +0 -230
- 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
- ksadk/tracing/exporters/langfuse_exporter.py +0 -445
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/top_level.txt +0 -0
ksadk/codex/runtime.py
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
"""CodexRuntimeAdapter — 非 ADK 体系的第三验证样本 (goal-09)。
|
|
2
|
+
|
|
3
|
+
对执行生命周期负责(不做 veadk 式薄桥接),后端能力面对齐 ``openai-codex`` SDK 真实线程模型
|
|
4
|
+
(``thread_start``/``thread.turn``/``handle.stream``/``handle.interrupt``/``thread_resume``):
|
|
5
|
+
|
|
6
|
+
- **cancel 状态机**(不薄委托给上层):活跃 turn → ``client.interrupt_active_turn``
|
|
7
|
+
(真实 SDK ``handle.interrupt``,终止当前 turn 执行;thread 由 codex 后端托管,无"杀进程"
|
|
8
|
+
概念);无活跃 turn → 记 pending 下个 turn 消费;**级联丢弃 pending 工具审批**(runtime
|
|
9
|
+
自跟踪的 pending 集);返回 ``CancelResult`` 枚举;**被中断的 turn 不持久化其 session**
|
|
10
|
+
(避免 resume 捡到写了一半的会话)。
|
|
11
|
+
- **phase 翻译**(:mod:`ksadk.codex.phase`):按 itemId 路由 commentary/final_answer delta,
|
|
12
|
+
映射 RuntimeEvent phase,不混入主逻辑。
|
|
13
|
+
- **resume 建模为 thread id**(真实 SDK ``thread_resume``),不套 ADK invocation 模型;
|
|
14
|
+
ResumeTarget(thread id)/ResumePayload(工具结果/HITL 回答)分离。
|
|
15
|
+
|
|
16
|
+
环境约束:read-only sandbox(默认),单轮含一次工具调用即可跑通。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any, AsyncIterator, Optional
|
|
26
|
+
|
|
27
|
+
from ksadk.codex.client import CodexClient
|
|
28
|
+
from ksadk.codex.phase import CodexPhaseTracker
|
|
29
|
+
from ksadk.events.runtime_event import EventType, RuntimeEvent
|
|
30
|
+
from ksadk.runtime.adapter import (
|
|
31
|
+
BaseRuntime,
|
|
32
|
+
CancelResult,
|
|
33
|
+
CheckpointCapability,
|
|
34
|
+
CheckpointDescriptor,
|
|
35
|
+
PauseResult,
|
|
36
|
+
ResumePayload,
|
|
37
|
+
ResumeTarget,
|
|
38
|
+
RunHandle,
|
|
39
|
+
RuntimeAdapter,
|
|
40
|
+
StartRequest,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
_INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _CodexAsBaseRuntime(BaseRuntime):
|
|
49
|
+
"""把 CodexClient 包装为 BaseRuntime(原生能力面)。"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, client: CodexClient) -> None:
|
|
52
|
+
self._client = client
|
|
53
|
+
self.runtime_type = "codex"
|
|
54
|
+
|
|
55
|
+
def native_capabilities(self) -> dict[str, Any]:
|
|
56
|
+
return {
|
|
57
|
+
"Framework": "codex",
|
|
58
|
+
"cancel": "thread",
|
|
59
|
+
"pause": "interrupt_then_resume_thread",
|
|
60
|
+
"resume": "thread_id",
|
|
61
|
+
"live_interaction": True,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class _CodexThread:
|
|
67
|
+
thread_id: str
|
|
68
|
+
turn_id: Optional[str] = None
|
|
69
|
+
streaming: bool = False
|
|
70
|
+
interrupt_event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
71
|
+
pending_approvals: set[str] = field(default_factory=set)
|
|
72
|
+
done: bool = False
|
|
73
|
+
interrupted: bool = False
|
|
74
|
+
paused: bool = False
|
|
75
|
+
started_at: int | None = None
|
|
76
|
+
completed_at: int | None = None
|
|
77
|
+
duration_ms: int | None = None
|
|
78
|
+
goal_mode: bool = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CodexRuntimeAdapter(RuntimeAdapter):
|
|
82
|
+
"""Codex 的 RuntimeAdapter(重托管)。"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
client: CodexClient,
|
|
87
|
+
*,
|
|
88
|
+
sandbox_read_only: bool = True,
|
|
89
|
+
turn_timeout_seconds: Optional[float] = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
super().__init__(_CodexAsBaseRuntime(client))
|
|
92
|
+
self._client = client
|
|
93
|
+
self._sandbox_read_only = sandbox_read_only
|
|
94
|
+
self._turn_timeout_seconds = turn_timeout_seconds
|
|
95
|
+
self._threads: dict[str, _CodexThread] = {}
|
|
96
|
+
self._requests: dict[str, StartRequest] = {}
|
|
97
|
+
self._known_threads: set[str] = set()
|
|
98
|
+
self._pending_cancels: set[str] = set()
|
|
99
|
+
# 被杀/interrupt 的 session 不持久化(goal-09 契约 1)。
|
|
100
|
+
self._do_not_persist: set[str] = set()
|
|
101
|
+
# 可观测:最近一次 cancel 级联丢弃的审批集(contract test 断言用)。
|
|
102
|
+
self.last_cancel_dropped_approvals: set[str] = set()
|
|
103
|
+
self._seq = 0
|
|
104
|
+
|
|
105
|
+
# ---- 六动词 ----
|
|
106
|
+
|
|
107
|
+
async def start(self, request: StartRequest) -> RunHandle:
|
|
108
|
+
# 新 thread 由后端分配真实 thread_id(thread_start);metadata 携带的 thread_id
|
|
109
|
+
# 表示接入既有 thread(resume 语义,run_turn 时按 resume 接入)。
|
|
110
|
+
provided = request.metadata.get("thread_id")
|
|
111
|
+
if provided:
|
|
112
|
+
thread_id = str(provided)
|
|
113
|
+
else:
|
|
114
|
+
# 把 model + base_instructions 传给 codex thread(配置契约,见 plan C)
|
|
115
|
+
thread_config: dict[str, Any] = {"sandbox_read_only": self._sandbox_read_only}
|
|
116
|
+
if request.config:
|
|
117
|
+
for key in ("sandbox", "approval_mode", "summary", "ephemeral"):
|
|
118
|
+
value = request.config.get(key)
|
|
119
|
+
if key == "ephemeral" and value is not None:
|
|
120
|
+
thread_config[key] = bool(value)
|
|
121
|
+
elif value:
|
|
122
|
+
thread_config[key] = value
|
|
123
|
+
if request.model:
|
|
124
|
+
thread_config["model"] = request.model
|
|
125
|
+
base_instructions = request.config.get("base_instructions")
|
|
126
|
+
if base_instructions:
|
|
127
|
+
thread_config["base_instructions"] = base_instructions
|
|
128
|
+
cwd = request.config.get("cwd")
|
|
129
|
+
if cwd:
|
|
130
|
+
thread_config["cwd"] = str(cwd)
|
|
131
|
+
thread_id = await self._client.start_thread(thread_config)
|
|
132
|
+
self._known_threads.add(thread_id)
|
|
133
|
+
thread = _CodexThread(thread_id=thread_id)
|
|
134
|
+
thread.__dict__["_start_request"] = request
|
|
135
|
+
self._threads[thread_id] = thread
|
|
136
|
+
self._requests[thread_id] = request
|
|
137
|
+
return RunHandle(
|
|
138
|
+
run_id=thread_id,
|
|
139
|
+
session_id=request.session_id,
|
|
140
|
+
runtime_type="codex",
|
|
141
|
+
native_ref={"thread_id": thread_id, "user_id": request.user_id},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def stream(self, handle: RunHandle) -> AsyncIterator[RuntimeEvent]:
|
|
145
|
+
return self._stream_events(handle)
|
|
146
|
+
|
|
147
|
+
async def cancel(self, handle: RunHandle) -> CancelResult:
|
|
148
|
+
thread_id = handle.run_id
|
|
149
|
+
thread = self._threads.get(thread_id)
|
|
150
|
+
is_active = thread is not None and not thread.done and thread.streaming
|
|
151
|
+
if not is_active:
|
|
152
|
+
if thread_id in self._known_threads:
|
|
153
|
+
self._pending_cancels.add(thread_id)
|
|
154
|
+
return CancelResult.PENDING_CANCEL_RECORDED
|
|
155
|
+
return CancelResult.NOT_RUNNING
|
|
156
|
+
assert thread is not None
|
|
157
|
+
try:
|
|
158
|
+
# 级联丢弃 pending 工具审批(快照 runtime 自跟踪的 pending 集,供观测)。
|
|
159
|
+
# 真实 SDK 无独立 drain API:interrupt 后 turn 停止,pending 审批随之失效。
|
|
160
|
+
self.last_cancel_dropped_approvals = set(thread.pending_approvals)
|
|
161
|
+
thread.pending_approvals.clear()
|
|
162
|
+
# 真实中断:handle.interrupt() 停活跃 turn(真实 SDK 机制;无"杀进程"概念,
|
|
163
|
+
# thread 由 codex 后端托管,interrupt 即终止当前 turn 的执行)。
|
|
164
|
+
# 先唤醒 Studio 的 stream;等待 request_user_input 时,中断 RPC
|
|
165
|
+
# 可能要等 turn 状态前进,否则取消接口与流会互相等待。
|
|
166
|
+
thread.interrupt_event.set()
|
|
167
|
+
interrupted = (
|
|
168
|
+
await self._client.cancel_goal(thread.thread_id)
|
|
169
|
+
if thread.goal_mode
|
|
170
|
+
else await self._client.interrupt_active_turn(thread.thread_id)
|
|
171
|
+
)
|
|
172
|
+
if not interrupted:
|
|
173
|
+
# 无活跃 handle 可 interrupt(竞态:turn 刚好结束)→ 视为未在运行。
|
|
174
|
+
return CancelResult.NOT_RUNNING
|
|
175
|
+
# 被中断的 session 不持久化(goal-09 契约 1)。
|
|
176
|
+
self._do_not_persist.add(thread_id)
|
|
177
|
+
thread.done = True
|
|
178
|
+
self._threads.pop(thread_id, None)
|
|
179
|
+
self._requests.pop(thread_id, None)
|
|
180
|
+
self._pending_cancels.discard(thread_id)
|
|
181
|
+
return CancelResult.INTERRUPTED_ACTIVE_TURN
|
|
182
|
+
except Exception: # noqa: BLE001
|
|
183
|
+
logger.exception("codex cancel thread %s 失败", thread_id)
|
|
184
|
+
return CancelResult.FAILED
|
|
185
|
+
|
|
186
|
+
async def pause(self, handle: RunHandle) -> PauseResult:
|
|
187
|
+
thread = self._threads.get(handle.run_id)
|
|
188
|
+
if thread is None or thread.done or not thread.streaming:
|
|
189
|
+
return PauseResult.NOT_RUNNING
|
|
190
|
+
if thread.pending_approvals:
|
|
191
|
+
# A blocked native approval must be answered through the interaction
|
|
192
|
+
# card. Interrupting while the SDK reader waits on that approval can
|
|
193
|
+
# deadlock the JSON-RPC command channel.
|
|
194
|
+
return PauseResult.FAILED
|
|
195
|
+
try:
|
|
196
|
+
interrupted = (
|
|
197
|
+
await self._client.pause_goal(thread.thread_id)
|
|
198
|
+
if thread.goal_mode
|
|
199
|
+
else await self._client.interrupt_active_turn(thread.thread_id)
|
|
200
|
+
)
|
|
201
|
+
if not interrupted:
|
|
202
|
+
return PauseResult.NOT_RUNNING
|
|
203
|
+
thread.paused = True
|
|
204
|
+
# ``thread/goal/set(status=paused)`` only prevents the next
|
|
205
|
+
# continuation. Wake the adapter stream as well so the current
|
|
206
|
+
# physical goal turn becomes an honest resumable pause now.
|
|
207
|
+
thread.interrupt_event.set()
|
|
208
|
+
return PauseResult.PAUSED_ACTIVE_TURN
|
|
209
|
+
except Exception: # noqa: BLE001
|
|
210
|
+
logger.exception("codex pause thread %s 失败", handle.run_id)
|
|
211
|
+
return PauseResult.FAILED
|
|
212
|
+
|
|
213
|
+
async def submit(self, handle: RunHandle, payload: ResumePayload) -> None:
|
|
214
|
+
if not payload.call_id:
|
|
215
|
+
raise ValueError("Codex live submit requires call_id")
|
|
216
|
+
raw = payload.data if isinstance(payload.data, dict) else {"decision": payload.data}
|
|
217
|
+
if payload.kind == "approval_decision":
|
|
218
|
+
decision = str(raw.get("decision") or raw.get("name") or "")
|
|
219
|
+
resolved = await self._client.resolve_approval(payload.call_id, decision)
|
|
220
|
+
elif payload.kind == "hitl_answer":
|
|
221
|
+
resolved = await self._client.resolve_interaction(
|
|
222
|
+
payload.call_id,
|
|
223
|
+
{key: value for key, value in raw.items() if key != "decision"},
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError("Codex live submit requires approval_decision or hitl_answer")
|
|
227
|
+
if not resolved:
|
|
228
|
+
raise ValueError(f"interaction {payload.call_id!r} is not pending")
|
|
229
|
+
thread = self._threads.get(handle.run_id)
|
|
230
|
+
if thread is not None:
|
|
231
|
+
thread.pending_approvals.discard(payload.call_id)
|
|
232
|
+
|
|
233
|
+
async def resume(
|
|
234
|
+
self,
|
|
235
|
+
handle: RunHandle,
|
|
236
|
+
target: ResumeTarget,
|
|
237
|
+
payload: Optional[ResumePayload],
|
|
238
|
+
) -> RunHandle:
|
|
239
|
+
# resume 用 thread id 语义(resume_thread_id),不套 ADK invocation 模型。
|
|
240
|
+
if target.kind != "thread_id":
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"CodexRuntimeAdapter resume 仅支持 thread_id 目标,得到 {target.kind!r}"
|
|
243
|
+
)
|
|
244
|
+
if handle.run_id in self._do_not_persist:
|
|
245
|
+
raise ValueError(f"thread {handle.run_id} 已被中断/杀进程,不持久化,不可 resume")
|
|
246
|
+
self._pending_cancels.discard(handle.run_id)
|
|
247
|
+
self._known_threads.add(target.id)
|
|
248
|
+
thread = _CodexThread(thread_id=target.id)
|
|
249
|
+
thread.__dict__["_resume"] = {"target": target, "payload": payload}
|
|
250
|
+
request = self._requests.get(handle.run_id)
|
|
251
|
+
if request is not None:
|
|
252
|
+
thread.__dict__["_request_config"] = request
|
|
253
|
+
if request is not None and str(request.config.get("goal_objective") or "").strip():
|
|
254
|
+
# Restart the same native Goal operation on the persisted thread;
|
|
255
|
+
# its transcript remains in Codex, while the objective/config are
|
|
256
|
+
# needed to resume Goal rather than issuing a normal text turn.
|
|
257
|
+
thread.goal_mode = True
|
|
258
|
+
self._threads[handle.run_id] = thread
|
|
259
|
+
# 真实恢复:thread_resume 接入后端既有 thread(thread_id 语义,不套 ADK invocation)。
|
|
260
|
+
resume_config: dict[str, Any] = {"sandbox_read_only": self._sandbox_read_only}
|
|
261
|
+
if request is not None and request.config:
|
|
262
|
+
for key in ("sandbox", "approval_mode"):
|
|
263
|
+
value = request.config.get(key)
|
|
264
|
+
if value:
|
|
265
|
+
resume_config[key] = value
|
|
266
|
+
await self._client.resume_thread(target.id, resume_config)
|
|
267
|
+
handle.native_ref["thread_id"] = target.id
|
|
268
|
+
handle.native_ref["resume_thread_id"] = target.id
|
|
269
|
+
handle.native_ref["resume_payload"] = payload.data if payload else None
|
|
270
|
+
# 与 ADK/LangGraph 一致的 resume_input 结构(供共用 contract test 断言)。
|
|
271
|
+
handle.native_ref["resume_input"] = {
|
|
272
|
+
"type": "codex.resume_thread",
|
|
273
|
+
"thread_id": target.id,
|
|
274
|
+
"payload": payload.data if payload else None,
|
|
275
|
+
"payload_kind": payload.kind if payload else None,
|
|
276
|
+
"call_id": payload.call_id if payload else None,
|
|
277
|
+
}
|
|
278
|
+
return handle
|
|
279
|
+
|
|
280
|
+
async def checkpoint(self, handle: RunHandle) -> CheckpointDescriptor:
|
|
281
|
+
return CheckpointDescriptor(
|
|
282
|
+
checkpoint_id=str(handle.native_ref.get("thread_id") or handle.run_id),
|
|
283
|
+
invocation_id=handle.run_id,
|
|
284
|
+
capability=CheckpointCapability(
|
|
285
|
+
supported=True,
|
|
286
|
+
granularity="snapshot",
|
|
287
|
+
rollback_scope="turn",
|
|
288
|
+
fork_supported=True,
|
|
289
|
+
durable=False,
|
|
290
|
+
shared_across_pods=False,
|
|
291
|
+
reason="Codex resume/fork by thread id",
|
|
292
|
+
),
|
|
293
|
+
ref={"thread_id": handle.native_ref.get("thread_id")},
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
async def close(self, handle: RunHandle) -> None:
|
|
297
|
+
thread = self._threads.pop(handle.run_id, None)
|
|
298
|
+
self._requests.pop(handle.run_id, None)
|
|
299
|
+
if thread is not None:
|
|
300
|
+
thread.interrupt_event.set()
|
|
301
|
+
try:
|
|
302
|
+
active_thread_id = thread.thread_id if thread is not None else handle.run_id
|
|
303
|
+
await self._client.interrupt_active_turn(active_thread_id)
|
|
304
|
+
finally:
|
|
305
|
+
# AsyncCodex.close owns terminate/wait/kill for the app-server child.
|
|
306
|
+
await self._client.close()
|
|
307
|
+
self._do_not_persist.add(handle.run_id)
|
|
308
|
+
self._known_threads.discard(handle.run_id)
|
|
309
|
+
self._pending_cancels.discard(handle.run_id)
|
|
310
|
+
|
|
311
|
+
# ---- stream → RuntimeEvent(phase 翻译 + 中断竞速) ----
|
|
312
|
+
|
|
313
|
+
def _next_thread_id(self) -> str:
|
|
314
|
+
self._seq += 1
|
|
315
|
+
return f"codex_thread_{self._seq}"
|
|
316
|
+
|
|
317
|
+
def _next_seq(self) -> int:
|
|
318
|
+
self._seq += 1
|
|
319
|
+
return self._seq
|
|
320
|
+
|
|
321
|
+
async def _stream_events(self, handle: RunHandle) -> AsyncIterator[RuntimeEvent]:
|
|
322
|
+
thread = self._threads.get(handle.run_id)
|
|
323
|
+
if thread is None:
|
|
324
|
+
thread = _CodexThread(thread_id=handle.run_id)
|
|
325
|
+
self._threads[handle.run_id] = thread
|
|
326
|
+
|
|
327
|
+
if handle.run_id in self._pending_cancels:
|
|
328
|
+
self._pending_cancels.discard(handle.run_id)
|
|
329
|
+
yield self._event(
|
|
330
|
+
handle,
|
|
331
|
+
EventType.RUN_CANCELED,
|
|
332
|
+
{
|
|
333
|
+
"status": "cancelled",
|
|
334
|
+
"cancel_result": CancelResult.PENDING_CANCEL_RECORDED.value,
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
yield self._event(handle, EventType.RUN_STARTED, {"status": "in_progress"})
|
|
340
|
+
tracker = CodexPhaseTracker()
|
|
341
|
+
request = thread.__dict__.get("_start_request")
|
|
342
|
+
resume_state = thread.__dict__.get("_resume")
|
|
343
|
+
if request is not None:
|
|
344
|
+
prompt = _request_prompt(request)
|
|
345
|
+
elif resume_state is not None:
|
|
346
|
+
prompt = _resume_prompt(resume_state.get("payload"))
|
|
347
|
+
else:
|
|
348
|
+
prompt = ""
|
|
349
|
+
run_input = _build_run_input(request, prompt)
|
|
350
|
+
thread.streaming = True
|
|
351
|
+
thread.turn_id = thread.turn_id or f"turn_{thread.thread_id}"
|
|
352
|
+
try:
|
|
353
|
+
async for event in self._map_codex_stream(handle, thread, tracker, run_input):
|
|
354
|
+
yield event
|
|
355
|
+
# 正常结束(非 interrupt):补 RUN_COMPLETED(AGUI 投射器据此发 RunFinished success)
|
|
356
|
+
if not thread.interrupted:
|
|
357
|
+
completed_payload: dict[str, Any] = {
|
|
358
|
+
"status": "completed",
|
|
359
|
+
"source": "codex",
|
|
360
|
+
}
|
|
361
|
+
if thread.started_at is not None:
|
|
362
|
+
completed_payload["started_at"] = thread.started_at
|
|
363
|
+
if thread.completed_at is not None:
|
|
364
|
+
completed_payload["completed_at"] = thread.completed_at
|
|
365
|
+
if thread.duration_ms is not None:
|
|
366
|
+
completed_payload["duration_ms"] = thread.duration_ms
|
|
367
|
+
yield self._event(handle, EventType.RUN_COMPLETED, completed_payload)
|
|
368
|
+
except asyncio.CancelledError:
|
|
369
|
+
thread.interrupted = True
|
|
370
|
+
self._do_not_persist.add(handle.run_id)
|
|
371
|
+
await self._client.interrupt_active_turn(thread.thread_id)
|
|
372
|
+
raise
|
|
373
|
+
except TimeoutError:
|
|
374
|
+
self.last_cancel_dropped_approvals = set(thread.pending_approvals)
|
|
375
|
+
thread.pending_approvals.clear()
|
|
376
|
+
self._do_not_persist.add(handle.run_id)
|
|
377
|
+
# Closing the SDK transport terminates and waits for the app-server
|
|
378
|
+
# child even when the stream is stuck between notifications.
|
|
379
|
+
await self._client.close()
|
|
380
|
+
yield self._event(
|
|
381
|
+
handle,
|
|
382
|
+
EventType.RUN_FAILED,
|
|
383
|
+
{"status": "failed", "error": "codex turn timed out"},
|
|
384
|
+
)
|
|
385
|
+
except Exception as exc: # noqa: BLE001 通用兜底:任何异常都发 RUN_FAILED
|
|
386
|
+
self._do_not_persist.add(handle.run_id)
|
|
387
|
+
yield self._event(
|
|
388
|
+
handle,
|
|
389
|
+
EventType.RUN_FAILED,
|
|
390
|
+
{"status": "failed", "error": str(exc)},
|
|
391
|
+
)
|
|
392
|
+
finally:
|
|
393
|
+
thread.streaming = False
|
|
394
|
+
thread.done = True
|
|
395
|
+
self._threads.pop(handle.run_id, None)
|
|
396
|
+
|
|
397
|
+
async def _map_codex_stream(
|
|
398
|
+
self,
|
|
399
|
+
handle: RunHandle,
|
|
400
|
+
thread: _CodexThread,
|
|
401
|
+
tracker: CodexPhaseTracker,
|
|
402
|
+
prompt: Any,
|
|
403
|
+
) -> AsyncIterator[RuntimeEvent]:
|
|
404
|
+
request = thread.__dict__.get("_start_request") or thread.__dict__.get("_request_config")
|
|
405
|
+
run_config: dict[str, Any] = {"sandbox_read_only": self._sandbox_read_only}
|
|
406
|
+
if request is not None and request.config:
|
|
407
|
+
for key in ("sandbox", "approval_mode", "summary", "collaboration_mode"):
|
|
408
|
+
value = request.config.get(key)
|
|
409
|
+
if key == "approval_mode" and value == "manual":
|
|
410
|
+
continue
|
|
411
|
+
if value:
|
|
412
|
+
run_config[key] = value
|
|
413
|
+
if request is not None and request.model:
|
|
414
|
+
run_config["model"] = request.model
|
|
415
|
+
goal_objective = str(
|
|
416
|
+
request.config.get("goal_objective") or ""
|
|
417
|
+
if request is not None and request.config
|
|
418
|
+
else ""
|
|
419
|
+
).strip()
|
|
420
|
+
if goal_objective:
|
|
421
|
+
thread.goal_mode = True
|
|
422
|
+
codex_gen = self._client.run_goal(
|
|
423
|
+
thread.thread_id,
|
|
424
|
+
goal_objective,
|
|
425
|
+
config=run_config,
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
codex_gen = self._client.run_turn(
|
|
429
|
+
thread.thread_id,
|
|
430
|
+
prompt,
|
|
431
|
+
config=run_config,
|
|
432
|
+
)
|
|
433
|
+
deadline = (
|
|
434
|
+
asyncio.get_running_loop().time() + self._turn_timeout_seconds
|
|
435
|
+
if self._turn_timeout_seconds is not None
|
|
436
|
+
else None
|
|
437
|
+
)
|
|
438
|
+
chunk_task: asyncio.Task[Any] | None = None
|
|
439
|
+
interrupt_task: asyncio.Task[bool] | None = None
|
|
440
|
+
try:
|
|
441
|
+
while True:
|
|
442
|
+
chunk_task = asyncio.ensure_future(_anext_or_stop(codex_gen))
|
|
443
|
+
interrupt_task = asyncio.ensure_future(thread.interrupt_event.wait())
|
|
444
|
+
remaining = (
|
|
445
|
+
max(0.0, deadline - asyncio.get_running_loop().time())
|
|
446
|
+
if deadline is not None
|
|
447
|
+
else None
|
|
448
|
+
)
|
|
449
|
+
done, pending = await asyncio.wait(
|
|
450
|
+
{chunk_task, interrupt_task},
|
|
451
|
+
timeout=remaining,
|
|
452
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
453
|
+
)
|
|
454
|
+
if not done:
|
|
455
|
+
# Keep handle.stream's turn queue registered until transport
|
|
456
|
+
# close calls SDK MessageRouter.fail_all. Cancelling the
|
|
457
|
+
# asyncio.to_thread waiter first would strand queue.get in
|
|
458
|
+
# the executor and hang interpreter shutdown.
|
|
459
|
+
interrupt_task.cancel()
|
|
460
|
+
await self._client.close()
|
|
461
|
+
await asyncio.gather(chunk_task, interrupt_task, return_exceptions=True)
|
|
462
|
+
raise TimeoutError("codex turn timed out")
|
|
463
|
+
if interrupt_task in done:
|
|
464
|
+
# ``openai-codex`` implements ``next_turn_notification``
|
|
465
|
+
# with ``asyncio.to_thread(queue.get)``. Cancelling that
|
|
466
|
+
# awaiter unregisters its queue while the worker thread is
|
|
467
|
+
# still blocked, so neither a later transport close nor
|
|
468
|
+
# event-loop teardown can wake it. After interrupting a
|
|
469
|
+
# turn, keep the queue registered and drain the SDK's
|
|
470
|
+
# terminal notifications instead. If the backend fails to
|
|
471
|
+
# finish promptly, close the transport *before* awaiting
|
|
472
|
+
# the waiter so MessageRouter.fail_all can release it.
|
|
473
|
+
drain_deadline = (
|
|
474
|
+
asyncio.get_running_loop().time()
|
|
475
|
+
+ _INTERRUPT_DRAIN_TIMEOUT_SECONDS
|
|
476
|
+
)
|
|
477
|
+
while True:
|
|
478
|
+
drain_remaining = max(
|
|
479
|
+
0.0,
|
|
480
|
+
drain_deadline - asyncio.get_running_loop().time(),
|
|
481
|
+
)
|
|
482
|
+
drained, _ = await asyncio.wait(
|
|
483
|
+
{chunk_task},
|
|
484
|
+
timeout=drain_remaining,
|
|
485
|
+
)
|
|
486
|
+
if not drained:
|
|
487
|
+
await self._client.close()
|
|
488
|
+
await asyncio.gather(chunk_task, return_exceptions=True)
|
|
489
|
+
break
|
|
490
|
+
try:
|
|
491
|
+
drained_chunk = chunk_task.result()
|
|
492
|
+
except Exception: # noqa: BLE001
|
|
493
|
+
break
|
|
494
|
+
if drained_chunk is _STREAM_STOP:
|
|
495
|
+
break
|
|
496
|
+
chunk_task = asyncio.ensure_future(_anext_or_stop(codex_gen))
|
|
497
|
+
chunk_task = None
|
|
498
|
+
thread.interrupted = True
|
|
499
|
+
# AGUI 投射器对 RUN_INTERRUPTED 无兜底,必须显式发,否则 raise
|
|
500
|
+
yield self._event(
|
|
501
|
+
handle,
|
|
502
|
+
EventType.RUN_INTERRUPTED,
|
|
503
|
+
{
|
|
504
|
+
"status": "paused" if thread.paused else "interrupted",
|
|
505
|
+
"reason": "user_pause" if thread.paused else "runtime_interrupt",
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
return
|
|
509
|
+
for task in pending:
|
|
510
|
+
task.cancel()
|
|
511
|
+
if pending:
|
|
512
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
513
|
+
chunk = chunk_task.result()
|
|
514
|
+
if chunk is _STREAM_STOP:
|
|
515
|
+
return
|
|
516
|
+
event = self._codex_chunk_to_event(handle, thread, tracker, chunk)
|
|
517
|
+
if event is not None:
|
|
518
|
+
yield event
|
|
519
|
+
finally:
|
|
520
|
+
waiter_tasks = [task for task in (chunk_task, interrupt_task) if task is not None]
|
|
521
|
+
for task in waiter_tasks:
|
|
522
|
+
if not task.done():
|
|
523
|
+
task.cancel()
|
|
524
|
+
if waiter_tasks:
|
|
525
|
+
await asyncio.gather(*waiter_tasks, return_exceptions=True)
|
|
526
|
+
aclose = getattr(codex_gen, "aclose", None)
|
|
527
|
+
if callable(aclose):
|
|
528
|
+
try:
|
|
529
|
+
await aclose()
|
|
530
|
+
except Exception: # noqa: BLE001
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
def _codex_chunk_to_event(
|
|
534
|
+
self,
|
|
535
|
+
handle: RunHandle,
|
|
536
|
+
thread: _CodexThread,
|
|
537
|
+
tracker: CodexPhaseTracker,
|
|
538
|
+
chunk: dict[str, Any],
|
|
539
|
+
) -> Optional[RuntimeEvent]:
|
|
540
|
+
if not isinstance(chunk, dict):
|
|
541
|
+
return None
|
|
542
|
+
method = str(chunk.get("method") or chunk.get("type") or "")
|
|
543
|
+
params = chunk.get("params") or chunk
|
|
544
|
+
|
|
545
|
+
if method == "error":
|
|
546
|
+
raw_error = params.get("error") if isinstance(params, dict) else None
|
|
547
|
+
error = raw_error if isinstance(raw_error, dict) else {}
|
|
548
|
+
message = str(
|
|
549
|
+
error.get("message")
|
|
550
|
+
or (params.get("message") if isinstance(params, dict) else "")
|
|
551
|
+
or raw_error
|
|
552
|
+
or "Codex runtime transport failed"
|
|
553
|
+
)
|
|
554
|
+
if not bool(params.get("will_retry") or params.get("willRetry")) or "401" in message:
|
|
555
|
+
raise RuntimeError(message)
|
|
556
|
+
return None
|
|
557
|
+
|
|
558
|
+
if method == "thread/tokenUsage/updated":
|
|
559
|
+
token_usage = params.get("token_usage") or params.get("tokenUsage") or {}
|
|
560
|
+
last = token_usage.get("last") if isinstance(token_usage, dict) else {}
|
|
561
|
+
if not isinstance(last, dict):
|
|
562
|
+
last = {}
|
|
563
|
+
return self._event(
|
|
564
|
+
handle,
|
|
565
|
+
EventType.USAGE_REPORTED,
|
|
566
|
+
{
|
|
567
|
+
"input_tokens": int(last.get("input_tokens", last.get("inputTokens", 0)) or 0),
|
|
568
|
+
"cached_tokens": int(
|
|
569
|
+
last.get("cached_input_tokens", last.get("cachedInputTokens", 0)) or 0
|
|
570
|
+
),
|
|
571
|
+
"output_tokens": int(
|
|
572
|
+
last.get("output_tokens", last.get("outputTokens", 0)) or 0
|
|
573
|
+
),
|
|
574
|
+
"reasoning_tokens": int(
|
|
575
|
+
last.get(
|
|
576
|
+
"reasoning_output_tokens",
|
|
577
|
+
last.get("reasoningOutputTokens", 0),
|
|
578
|
+
)
|
|
579
|
+
or 0
|
|
580
|
+
),
|
|
581
|
+
"total_tokens": int(last.get("total_tokens", last.get("totalTokens", 0)) or 0),
|
|
582
|
+
"source": "codex",
|
|
583
|
+
},
|
|
584
|
+
)
|
|
585
|
+
if method == "thread/goal/updated":
|
|
586
|
+
goal = params.get("goal") if isinstance(params, dict) else {}
|
|
587
|
+
goal = goal if isinstance(goal, dict) else {}
|
|
588
|
+
status = str(goal.get("status") or "").lower()
|
|
589
|
+
if status in {"paused", "blocked", "usage_limited", "budget_limited"}:
|
|
590
|
+
thread.paused = status == "paused"
|
|
591
|
+
thread.interrupted = True
|
|
592
|
+
return self._event(
|
|
593
|
+
handle,
|
|
594
|
+
EventType.RUN_INTERRUPTED,
|
|
595
|
+
{"status": status, "reason": "goal_status", "goal": goal},
|
|
596
|
+
)
|
|
597
|
+
return self._event(
|
|
598
|
+
handle,
|
|
599
|
+
EventType.RUN_PROGRESS,
|
|
600
|
+
{"native_event": "goal.updated", "native_data": goal},
|
|
601
|
+
)
|
|
602
|
+
if method in {"turn/started", "turn/completed"}:
|
|
603
|
+
raw_turn = params.get("turn")
|
|
604
|
+
turn: dict[str, Any] = raw_turn if isinstance(raw_turn, dict) else {}
|
|
605
|
+
started_at = turn.get("started_at", turn.get("startedAt"))
|
|
606
|
+
completed_at = turn.get("completed_at", turn.get("completedAt"))
|
|
607
|
+
duration_ms = turn.get("duration_ms", turn.get("durationMs"))
|
|
608
|
+
if started_at is not None:
|
|
609
|
+
thread.started_at = int(started_at)
|
|
610
|
+
if completed_at is not None:
|
|
611
|
+
thread.completed_at = int(completed_at)
|
|
612
|
+
if duration_ms is not None:
|
|
613
|
+
thread.duration_ms = max(0, int(duration_ms))
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
if method == "a2ui/surface":
|
|
617
|
+
surface_id = str(params.get("surface_id") or params.get("surfaceId") or "")
|
|
618
|
+
return self._event(
|
|
619
|
+
handle,
|
|
620
|
+
EventType.A2UI_SURFACE_BEGIN,
|
|
621
|
+
{
|
|
622
|
+
"surface_id": surface_id,
|
|
623
|
+
"surface": params.get("surface")
|
|
624
|
+
if isinstance(params.get("surface"), dict)
|
|
625
|
+
else {},
|
|
626
|
+
},
|
|
627
|
+
)
|
|
628
|
+
if method == "a2ui/interaction":
|
|
629
|
+
interaction_id = str(params.get("interaction_id") or params.get("interactionId") or "")
|
|
630
|
+
if interaction_id:
|
|
631
|
+
thread.pending_approvals.add(interaction_id)
|
|
632
|
+
return self._event(
|
|
633
|
+
handle,
|
|
634
|
+
EventType.A2UI_INTERACTION,
|
|
635
|
+
{
|
|
636
|
+
"surface_id": str(params.get("surface_id") or params.get("surfaceId") or ""),
|
|
637
|
+
"interaction_id": interaction_id,
|
|
638
|
+
"kind": str(params.get("kind") or "form"),
|
|
639
|
+
"input_schema": params.get("input_schema")
|
|
640
|
+
if isinstance(params.get("input_schema"), dict)
|
|
641
|
+
else {},
|
|
642
|
+
"is_blocking": bool(params.get("is_blocking", True)),
|
|
643
|
+
},
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
if method == "item/started":
|
|
647
|
+
tracker.observe_item(params)
|
|
648
|
+
item = params.get("item") or params
|
|
649
|
+
if item.get("type") == "commandExecution":
|
|
650
|
+
call_id = str(item.get("id") or "")
|
|
651
|
+
return self._event(
|
|
652
|
+
handle,
|
|
653
|
+
EventType.TOOL_CALL_BEGIN,
|
|
654
|
+
{
|
|
655
|
+
"call_id": call_id,
|
|
656
|
+
"name": "codex.command",
|
|
657
|
+
"args": {
|
|
658
|
+
"command": str(item.get("command") or ""),
|
|
659
|
+
"cwd": str(item.get("cwd") or ""),
|
|
660
|
+
"command_actions": item.get("commandActions")
|
|
661
|
+
or item.get("command_actions")
|
|
662
|
+
or [],
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
)
|
|
666
|
+
if item.get("type") == "mcpToolCall":
|
|
667
|
+
call_id = str(item.get("id") or "")
|
|
668
|
+
server = str(item.get("server") or "")
|
|
669
|
+
tool = str(item.get("tool") or "")
|
|
670
|
+
return self._event(
|
|
671
|
+
handle,
|
|
672
|
+
EventType.TOOL_CALL_BEGIN,
|
|
673
|
+
{
|
|
674
|
+
"call_id": call_id,
|
|
675
|
+
"name": f"mcp.{server}.{tool}" if server else f"mcp.{tool}",
|
|
676
|
+
"args": {
|
|
677
|
+
"server": server,
|
|
678
|
+
"tool": tool,
|
|
679
|
+
"arguments": item.get("arguments"),
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
)
|
|
683
|
+
return None
|
|
684
|
+
if method == "item/completed":
|
|
685
|
+
item = params.get("item") or params
|
|
686
|
+
item_type = item.get("type")
|
|
687
|
+
if item_type == "commandExecution":
|
|
688
|
+
tracker.forget_item(params)
|
|
689
|
+
call_id = str(item.get("id") or "")
|
|
690
|
+
return self._event(
|
|
691
|
+
handle,
|
|
692
|
+
EventType.TOOL_CALL_END,
|
|
693
|
+
{
|
|
694
|
+
"call_id": call_id,
|
|
695
|
+
"name": "codex.command",
|
|
696
|
+
"result": {
|
|
697
|
+
"status": str(item.get("status") or "completed"),
|
|
698
|
+
"exit_code": item.get("exitCode", item.get("exit_code")),
|
|
699
|
+
"duration_ms": item.get("durationMs", item.get("duration_ms")),
|
|
700
|
+
"output": str(
|
|
701
|
+
item.get("aggregatedOutput") or item.get("aggregated_output") or ""
|
|
702
|
+
),
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
)
|
|
706
|
+
if item_type == "mcpToolCall":
|
|
707
|
+
tracker.forget_item(params)
|
|
708
|
+
call_id = str(item.get("id") or "")
|
|
709
|
+
server = str(item.get("server") or "")
|
|
710
|
+
tool = str(item.get("tool") or "")
|
|
711
|
+
raw_result = item.get("result")
|
|
712
|
+
result_obj: dict[str, Any] = raw_result if isinstance(raw_result, dict) else {}
|
|
713
|
+
raw_error = item.get("error")
|
|
714
|
+
error_obj: dict[str, Any] = raw_error if isinstance(raw_error, dict) else {}
|
|
715
|
+
output = self._mcp_result_text(result_obj)
|
|
716
|
+
error_message = str(error_obj.get("message") or "")
|
|
717
|
+
if not output and error_message:
|
|
718
|
+
output = error_message
|
|
719
|
+
return self._event(
|
|
720
|
+
handle,
|
|
721
|
+
EventType.TOOL_CALL_END,
|
|
722
|
+
{
|
|
723
|
+
"call_id": call_id,
|
|
724
|
+
"name": f"mcp.{server}.{tool}" if server else f"mcp.{tool}",
|
|
725
|
+
"result": {
|
|
726
|
+
"status": str(item.get("status") or "completed"),
|
|
727
|
+
"duration_ms": item.get("durationMs", item.get("duration_ms")),
|
|
728
|
+
"output": output,
|
|
729
|
+
**({"error": error_message} if error_message else {}),
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
)
|
|
733
|
+
if item_type != "agentMessage":
|
|
734
|
+
tracker.forget_item(params)
|
|
735
|
+
return None
|
|
736
|
+
phase = tracker.runtime_phase_for_item(params)
|
|
737
|
+
tracker.forget_item(params)
|
|
738
|
+
text = str(item.get("text") or "")
|
|
739
|
+
return self._event(
|
|
740
|
+
handle,
|
|
741
|
+
EventType.TEXT_COMPLETED,
|
|
742
|
+
{"text": text},
|
|
743
|
+
phase=phase or "final_answer",
|
|
744
|
+
)
|
|
745
|
+
if "delta" in method or "Delta" in method or method == "item/agentMessage/delta":
|
|
746
|
+
phase = tracker.runtime_phase_for_delta(params)
|
|
747
|
+
delta = str(params.get("delta") or "")
|
|
748
|
+
if not delta:
|
|
749
|
+
return None
|
|
750
|
+
return self._event(
|
|
751
|
+
handle, EventType.TEXT_DELTA, {"text": delta}, phase=phase or "commentary"
|
|
752
|
+
)
|
|
753
|
+
if method == "item/autoApprovalReview/started":
|
|
754
|
+
review_id = str(params.get("review_id") or params.get("reviewId") or "")
|
|
755
|
+
if review_id:
|
|
756
|
+
thread.pending_approvals.add(review_id)
|
|
757
|
+
return None
|
|
758
|
+
if method == "item/autoApprovalReview/completed":
|
|
759
|
+
review_id = str(params.get("review_id") or params.get("reviewId") or "")
|
|
760
|
+
thread.pending_approvals.discard(review_id)
|
|
761
|
+
return None
|
|
762
|
+
if (
|
|
763
|
+
"approval" in method.lower()
|
|
764
|
+
or "requestPermission" in method
|
|
765
|
+
or "approval" in str(chunk.get("type") or "").lower()
|
|
766
|
+
):
|
|
767
|
+
call_id = str(
|
|
768
|
+
params.get("id") or params.get("call_id") or params.get("requestId") or ""
|
|
769
|
+
)
|
|
770
|
+
if call_id:
|
|
771
|
+
thread.pending_approvals.add(call_id)
|
|
772
|
+
return self._event(
|
|
773
|
+
handle,
|
|
774
|
+
EventType.APPROVAL_REQUESTED,
|
|
775
|
+
{
|
|
776
|
+
"approval_id": call_id,
|
|
777
|
+
"call_id": call_id,
|
|
778
|
+
"kind": str(params.get("kind") or "tool"),
|
|
779
|
+
"detail": params.get("detail")
|
|
780
|
+
if isinstance(params.get("detail"), dict)
|
|
781
|
+
else params,
|
|
782
|
+
},
|
|
783
|
+
)
|
|
784
|
+
return None
|
|
785
|
+
|
|
786
|
+
def _event(
|
|
787
|
+
self,
|
|
788
|
+
handle: RunHandle,
|
|
789
|
+
event_type: str,
|
|
790
|
+
payload: dict,
|
|
791
|
+
*,
|
|
792
|
+
phase: Optional[str] = None,
|
|
793
|
+
) -> RuntimeEvent:
|
|
794
|
+
request = self._requests.get(handle.run_id)
|
|
795
|
+
return RuntimeEvent.create(
|
|
796
|
+
event_type,
|
|
797
|
+
agent_id=str(request.agent_id or "codex") if request is not None else "codex",
|
|
798
|
+
user_id=(
|
|
799
|
+
request.user_id
|
|
800
|
+
if request is not None
|
|
801
|
+
else str(handle.native_ref.get("user_id") or "user")
|
|
802
|
+
),
|
|
803
|
+
session_id=handle.session_id,
|
|
804
|
+
invocation_id=(
|
|
805
|
+
str(request.metadata.get("invocation_id") or handle.run_id)
|
|
806
|
+
if request is not None
|
|
807
|
+
else handle.run_id
|
|
808
|
+
),
|
|
809
|
+
seq_id=self._next_seq(),
|
|
810
|
+
phase=phase,
|
|
811
|
+
payload=payload,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
@staticmethod
|
|
815
|
+
def _mcp_result_text(result: dict[str, Any]) -> str:
|
|
816
|
+
"""把 MCP result.content 列表提取为可读文本;无文本时退化 structuredContent。"""
|
|
817
|
+
if not result:
|
|
818
|
+
return ""
|
|
819
|
+
content = result.get("content")
|
|
820
|
+
if isinstance(content, list):
|
|
821
|
+
texts = [
|
|
822
|
+
str(item.get("text") or "")
|
|
823
|
+
for item in content
|
|
824
|
+
if isinstance(item, dict) and item.get("type") == "text"
|
|
825
|
+
]
|
|
826
|
+
texts = [text for text in texts if text]
|
|
827
|
+
if texts:
|
|
828
|
+
return "\n".join(texts)
|
|
829
|
+
structured = result.get("structuredContent", result.get("structured_content"))
|
|
830
|
+
if structured is not None:
|
|
831
|
+
return json.dumps(structured, ensure_ascii=False)
|
|
832
|
+
return ""
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
_STREAM_STOP = object()
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
async def _anext_or_stop(gen: AsyncIterator[Any]) -> Any:
|
|
839
|
+
try:
|
|
840
|
+
return await gen.__anext__()
|
|
841
|
+
except StopAsyncIteration:
|
|
842
|
+
return _STREAM_STOP
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _resume_prompt(payload: Optional[ResumePayload]) -> Any:
|
|
846
|
+
if payload is None:
|
|
847
|
+
return ""
|
|
848
|
+
if isinstance(payload.data, str):
|
|
849
|
+
return payload.data
|
|
850
|
+
return json.dumps(payload.data, ensure_ascii=False, sort_keys=True)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _request_prompt(request: StartRequest) -> Any:
|
|
854
|
+
"""Render canonical conversation history for a native Codex turn.
|
|
855
|
+
|
|
856
|
+
只传 user 消息历史 + 当前 input,不传 assistant 的完整回复(避免 codex 复述历史)。
|
|
857
|
+
assistant 回复用简短摘要代替,仅维持对话结构。
|
|
858
|
+
"""
|
|
859
|
+
# A resumed Codex thread already owns its transcript. Re-sending Studio's
|
|
860
|
+
# transport-neutral history would duplicate every prior turn after refresh.
|
|
861
|
+
if str(request.metadata.get("thread_id") or "").strip():
|
|
862
|
+
return request.input
|
|
863
|
+
|
|
864
|
+
conversation = request.conversation_preprocessing()
|
|
865
|
+
if conversation is None or not conversation.messages:
|
|
866
|
+
return request.input
|
|
867
|
+
|
|
868
|
+
lines: list[str] = []
|
|
869
|
+
for message in conversation.messages:
|
|
870
|
+
content = message.get("content")
|
|
871
|
+
if not isinstance(content, str):
|
|
872
|
+
continue
|
|
873
|
+
role = str(message.get("role") or "user").strip().lower()
|
|
874
|
+
if role == "assistant":
|
|
875
|
+
# 不传完整回复,只标注"已回复",避免 codex 复述
|
|
876
|
+
summary = content[:80] + ("..." if len(content) > 80 else "")
|
|
877
|
+
lines.append(f"[上一轮已回复: {summary}]")
|
|
878
|
+
elif role == "user":
|
|
879
|
+
lines.append(f"User: {content}")
|
|
880
|
+
return "\n".join(lines) or request.input
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _build_run_input(request: Optional[StartRequest], prompt: Any) -> Any:
|
|
884
|
+
"""Compose Codex skills and native text/image/mention turn input."""
|
|
885
|
+
try:
|
|
886
|
+
from openai_codex import ImageInput, LocalImageInput, MentionInput, SkillInput, TextInput
|
|
887
|
+
except ImportError:
|
|
888
|
+
return prompt
|
|
889
|
+
skills: list[SkillInput] = []
|
|
890
|
+
if request is not None:
|
|
891
|
+
raw = request.config.get("skills") if request.config else None
|
|
892
|
+
if isinstance(raw, list):
|
|
893
|
+
for item in raw:
|
|
894
|
+
if not isinstance(item, dict):
|
|
895
|
+
continue
|
|
896
|
+
name = str(item.get("name") or "").strip()
|
|
897
|
+
path = str(item.get("path") or "").strip()
|
|
898
|
+
if name and path:
|
|
899
|
+
skills.append(SkillInput(name=name, path=path))
|
|
900
|
+
native_items: list[Any] = []
|
|
901
|
+
raw_input = request.input if request is not None else prompt
|
|
902
|
+
if isinstance(raw_input, list):
|
|
903
|
+
text_replaced = False
|
|
904
|
+
for item in raw_input:
|
|
905
|
+
if not isinstance(item, dict):
|
|
906
|
+
continue
|
|
907
|
+
kind = str(item.get("type") or "")
|
|
908
|
+
if kind == "text":
|
|
909
|
+
text = str(
|
|
910
|
+
prompt
|
|
911
|
+
if not text_replaced and isinstance(prompt, str)
|
|
912
|
+
else item.get("text") or ""
|
|
913
|
+
)
|
|
914
|
+
text_replaced = True
|
|
915
|
+
if text:
|
|
916
|
+
native_items.append(TextInput(text=text))
|
|
917
|
+
elif kind == "image" and item.get("url"):
|
|
918
|
+
native_items.append(ImageInput(url=str(item["url"])))
|
|
919
|
+
elif kind == "localImage" and item.get("path"):
|
|
920
|
+
native_items.append(LocalImageInput(path=str(item["path"])))
|
|
921
|
+
elif kind == "mention" and item.get("path"):
|
|
922
|
+
native_items.append(
|
|
923
|
+
MentionInput(
|
|
924
|
+
name=str(item.get("name") or item["path"]),
|
|
925
|
+
path=str(item["path"]),
|
|
926
|
+
)
|
|
927
|
+
)
|
|
928
|
+
else:
|
|
929
|
+
text = prompt if isinstance(prompt, str) else str(prompt or "")
|
|
930
|
+
if text:
|
|
931
|
+
native_items.append(TextInput(text=text))
|
|
932
|
+
combined = [*skills, *native_items]
|
|
933
|
+
if not combined:
|
|
934
|
+
return prompt
|
|
935
|
+
if len(combined) == 1 and isinstance(combined[0], TextInput) and not skills:
|
|
936
|
+
return combined[0].text
|
|
937
|
+
return combined
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
__all__ = ["CodexRuntimeAdapter"]
|